From bdf234d72804d984e286e4c559e1be5ef7455785 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 9 Jan 2022 22:28:07 -0500 Subject: [PATCH 001/244] 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 002/244] 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 5bf6c96f9fed04de949403202bafcbeadb5d2030 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 07:34:37 -0500 Subject: [PATCH 003/244] feat: setup a typescript build --- packages/desktop/src/index.html | 5 +- packages/desktop/src/index.js | 446 ------------------ packages/desktop/src/lib.rs | 2 +- packages/jsinterpreter/interpreter.js | 461 +++++++++++++++++++ packages/jsinterpreter/interpreter.ts | 636 ++++++++++++++++++++++++++ packages/jsinterpreter/tsconfig.json | 10 + 6 files changed, 1111 insertions(+), 449 deletions(-) delete mode 100644 packages/desktop/src/index.js create mode 100644 packages/jsinterpreter/interpreter.js create mode 100644 packages/jsinterpreter/interpreter.ts create mode 100644 packages/jsinterpreter/tsconfig.json diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index a67432e98..888ab8437 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -12,7 +12,8 @@
- + + + diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js deleted file mode 100644 index 88cf9fcdc..000000000 --- a/packages/desktop/src/index.js +++ /dev/null @@ -1,446 +0,0 @@ -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, -}; - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - - case "compositionend": - case "compositionstart": - case "compositionupdate": - return { - data: event.data, - }; - - case "keydown": - case "keypress": - case "keyup": - return { - char_code: event.charCode, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - locale: "locale", - location: event.location, - repeat: event.repeat, - which: event.which, - // locale: event.locale, - }; - - case "focus": - case "blur": - return {}; - - case "change": - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - - return { - value: value, - }; - - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - }; - } - - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - - case "select": - return {}; - - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - }; - - case "scroll": - return {}; - - case "wheel": - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - - case "animationstart": - case "animationend": - case "animationiteration": - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "transitionend": - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": - return {}; - - case "toggle": - return {}; - - default: - return {}; - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = { - onclick: {}, - }; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(edit) { - const id = edit.root; - const node = this.nodes[id]; - this.stack.push(node); - } - - AppendChildren(edit) { - let root = this.stack[this.stack.length - (1 + edit.many)]; - - let to_add = this.stack.splice(this.stack.length - edit.many); - - for (let i = 0; i < edit.many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(edit) { - let root = this.nodes[edit.root]; - let els = this.stack.splice(this.stack.length - edit.m); - - root.replaceWith(...els); - } - - InsertAfter(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.after(...new_nodes); - } - - InsertBefore(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.before(...new_nodes); - } - - Remove(edit) { - let node = this.nodes[edit.root]; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(edit) { - const node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; - this.stack.push(node); - } - - CreateElement(edit) { - const tagName = edit.tag; - const el = document.createElement(tagName); - this.nodes[edit.root] = el; - el.setAttribute("dioxus-id", edit.root); - this.stack.push(el); - } - - CreateElementNs(edit) { - let el = document.createElementNS(edit.ns, edit.tag); - this.stack.push(el); - this.nodes[edit.root] = el; - } - - CreatePlaceholder(edit) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[edit.root] = el; - } - - RemoveEventListener(edit) {} - - NewEventListener(edit) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - - const element = this.nodes[edit.root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = true; - - this.root.addEventListener(event_name, (event) => { - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } - - if (real_id == null) { - return; - } - - rpc.call("user_event", { - event: event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - }); - } - } - - SetText(edit) { - this.nodes[edit.root].textContent = edit.text; - } - - SetAttribute(edit) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; - - if (ns == "style") { - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - 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(edit) { - const name = edit.field; - const node = this.nodes[edit.root]; - node.removeAttribute(name); - - if (name === "value") { - node.value = null; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - - handleEdits(edits) { - this.stack.push(this.root); - - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = this[edit.type]; - f.call(this, edit); - } - } -} - -function main() { - let root = window.document.getElementById("main"); - window.interpreter = new Interpreter(root); - rpc.call("initialize"); -} - -main(); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..02e1dca82 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -203,7 +203,7 @@ pub fn launch_with_props( } else if path.trim_end_matches('/') == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("./index.js").to_vec()) + .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::NOT_FOUND) diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js new file mode 100644 index 000000000..ac2e7adb5 --- /dev/null +++ b/packages/jsinterpreter/interpreter.js @@ -0,0 +1,461 @@ +"use strict"; +exports.__esModule = true; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + case "compositionend": + case "compositionstart": + case "compositionupdate": + var data = event.data; + return { + data: data + }; + case "keydown": + case "keypress": + case "keyup": + var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale" + }; + case "focus": + case "blur": + return {}; + case "change": + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; + } + return { + value: value + }; + case "input": + case "invalid": + case "reset": + case "submit": { + var target_1 = event.target; + var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1 + }; + } + 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": { + var _d = event, altKey_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1 + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; + return { + alt_key: altKey_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + 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": { + var _f = event, altKey_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3 + }; + } + case "scroll": + return {}; + case "wheel": { + var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement + }; + } + case "transitionend": { + var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement; + 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 {}; + } +} +var 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 +}; +var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (edit) { + var id = edit.root; + var node = this.nodes[id]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (edit) { + var root = this.stack[this.stack.length - (1 + edit.many)]; + var to_add = this.stack.splice(this.stack.length - edit.many); + for (var i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (edit) { + var root = this.nodes[edit.root]; + var els = this.stack.splice(this.stack.length - edit.m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (edit) { + var node = this.nodes[edit.root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (edit) { + // todo: make it so the types are okay + var node = document.createTextNode(edit.text); + this.nodes[edit.root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (edit) { + var el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", "".concat(edit.root)); + this.nodes[edit.root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (edit) { + var el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (edit) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.RemoveEventListener = function (edit) { }; + Interpreter.prototype.NewEventListener = function (edit, handler) { + var event_name = edit.event_name; + var mounted_node_id = edit.root; + var scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + var element = this.nodes[edit.root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + }; + Interpreter.prototype.SetText = function (edit) { + this.nodes[edit.root].textContent = edit.text; + }; + Interpreter.prototype.SetAttribute = function (edit) { + // console.log("setting attr", edit); + var name = edit.field; + var value = edit.value; + var ns = edit.ns; + var node = this.nodes[edit.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); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (edit) { + var name = edit.name; + var node = this.nodes[edit.root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + console.log("handling edits ", edits); + this.stack.push(this.root); + var _loop_1 = function (edit) { + switch (edit.type) { + case "AppendChildren": + this_1.AppendChildren(edit); + break; + case "ReplaceWith": + this_1.ReplaceWith(edit); + break; + case "InsertAfter": + this_1.InsertAfter(edit); + break; + case "InsertBefore": + this_1.InsertBefore(edit); + break; + case "Remove": + this_1.Remove(edit); + break; + case "CreateTextNode": + this_1.CreateTextNode(edit); + break; + case "CreateElement": + this_1.CreateElement(edit); + break; + case "CreateElementNs": + this_1.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this_1.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this_1.RemoveEventListener(edit); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this_1.NewEventListener(edit, handler); + break; + case "SetText": + this_1.SetText(edit); + break; + case "SetAttribute": + this_1.SetAttribute(edit); + break; + case "RemoveAttribute": + this_1.RemoveAttribute(edit); + break; + } + }; + var this_1 = this; + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + _loop_1(edit); + } + }; + return Interpreter; +}()); +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +main(); diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts new file mode 100644 index 000000000..faf9e1dab --- /dev/null +++ b/packages/jsinterpreter/interpreter.ts @@ -0,0 +1,636 @@ + + +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, +}; + +class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: (event: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = { + }; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(edit: PushRoot) { + const id = edit.root; + const node = this.nodes[id]; + this.stack.push(node); + } + + AppendChildren(edit: AppendChildren) { + let root = this.stack[this.stack.length - (1 + edit.many)]; + + let to_add = this.stack.splice(this.stack.length - edit.many); + + for (let i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(edit: ReplaceWith) { + let root = this.nodes[edit.root] as Element; + let els = this.stack.splice(this.stack.length - edit.m); + + root.replaceWith(...els); + } + + InsertAfter(edit: InsertAfter) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after(...new_nodes); + } + + InsertBefore(edit: InsertBefore) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before(...new_nodes); + } + + Remove(edit: Remove) { + let node = this.nodes[edit.root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(edit: CreateTextNode) { + // todo: make it so the types are okay + const node = document.createTextNode(edit.text) as any as Element; + this.nodes[edit.root] = node; + this.stack.push(node); + } + + CreateElement(edit: CreateElement) { + const el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", `${edit.root}`); + + this.nodes[edit.root] = el; + this.stack.push(el); + } + + CreateElementNs(edit: CreateElementNs) { + let el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + } + + CreatePlaceholder(edit: CreatePlaceholder) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + } + + RemoveEventListener(edit: RemoveEventListener) { } + + NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { + const event_name = edit.event_name; + const mounted_node_id = edit.root; + const scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + + const element = this.nodes[edit.root]; + element.setAttribute( + `dioxus-event-${event_name}`, + `${scope}.${mounted_node_id}` + ); + + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + } + + SetText(edit: SetText) { + this.nodes[edit.root].textContent = edit.text; + } + + SetAttribute(edit: SetAttribute) { + // console.log("setting attr", edit); + const name = edit.field; + const value = edit.value; + const ns = edit.ns; + const node = this.nodes[edit.root]; + + if (ns == "style") { + + // @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(edit: RemoveAttribute) { + const name = edit.name; + + const node = this.nodes[edit.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[]) { + console.log("handling edits ", edits); + this.stack.push(this.root); + + for (let edit of edits) { + switch (edit.type) { + case "AppendChildren": + this.AppendChildren(edit); + break; + case "ReplaceWith": + this.ReplaceWith(edit); + break; + case "InsertAfter": + this.InsertAfter(edit); + break; + case "InsertBefore": + this.InsertBefore(edit); + break; + case "Remove": + this.Remove(edit); + break; + case "CreateTextNode": + this.CreateTextNode(edit); + break; + case "CreateElement": + this.CreateElement(edit); + break; + case "CreateElementNs": + this.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { + + const real_id = target.getAttribute(`dioxus-id`); + + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); + } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit); + break; + case "SetAttribute": + this.SetAttribute(edit); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit); + break; + } + } + } +} + +function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + + +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; + + +export { }; +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +type Edits = DomEdit[]; + +main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json new file mode 100644 index 000000000..698781908 --- /dev/null +++ b/packages/jsinterpreter/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + // "module": "None", + "module": "commonjs", + "strict": true, + "outDir": "dist", + "sourceMap": true + } +} From 10db6ad65bd23dabf60d0afbe6ff82cda8220a8b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 08:57:42 -0500 Subject: [PATCH 004/244] wip: web now links against the js interprter code --- packages/jsinterpreter/interpreter.js | 243 ++++---- packages/jsinterpreter/interpreter.ts | 251 ++++---- packages/jsinterpreter/tsconfig.json | 1 - packages/web/Cargo.toml | 4 +- packages/web/src/bindings.rs | 59 ++ packages/web/src/dom.rs | 419 +------------- packages/web/src/interpreter.js | 529 +++++++++++++++++ packages/web/src/lib.rs | 1 + packages/web/src/olddom.rs | 789 ++++++++++++++++++++++++++ packages/web/src/rehydrate.rs | 10 +- 10 files changed, 1661 insertions(+), 645 deletions(-) create mode 100644 packages/web/src/bindings.rs create mode 100644 packages/web/src/interpreter.js create mode 100644 packages/web/src/olddom.rs diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index ac2e7adb5..6139280f8 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,5 +1,6 @@ "use strict"; exports.__esModule = true; +exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { @@ -247,84 +248,79 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.pop = function () { return this.stack.pop(); }; - Interpreter.prototype.PushRoot = function (edit) { - var id = edit.root; - var node = this.nodes[id]; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; this.stack.push(node); }; - Interpreter.prototype.AppendChildren = function (edit) { - var root = this.stack[this.stack.length - (1 + edit.many)]; - var to_add = this.stack.splice(this.stack.length - edit.many); - for (var i = 0; i < edit.many; i++) { + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { root.appendChild(to_add[i]); } }; - Interpreter.prototype.ReplaceWith = function (edit) { - var root = this.nodes[edit.root]; - var els = this.stack.splice(this.stack.length - edit.m); + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); root.replaceWith.apply(root, els); }; - Interpreter.prototype.InsertAfter = function (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); old.after.apply(old, new_nodes); }; - Interpreter.prototype.InsertBefore = function (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); old.before.apply(old, new_nodes); }; - Interpreter.prototype.Remove = function (edit) { - var node = this.nodes[edit.root]; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; if (node !== undefined) { node.remove(); } }; - Interpreter.prototype.CreateTextNode = function (edit) { + Interpreter.prototype.CreateTextNode = function (text, root) { // todo: make it so the types are okay - var node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; + var node = document.createTextNode(text); + this.nodes[root] = node; this.stack.push(node); }; - Interpreter.prototype.CreateElement = function (edit) { - var el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", "".concat(edit.root)); - this.nodes[edit.root] = el; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; this.stack.push(el); }; - Interpreter.prototype.CreateElementNs = function (edit) { - var el = document.createElementNS(edit.ns, edit.tag); + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.CreatePlaceholder = function (edit) { + Interpreter.prototype.CreatePlaceholder = function (root) { var el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.RemoveEventListener = function (edit) { }; - Interpreter.prototype.NewEventListener = function (edit, handler) { - var event_name = edit.event_name; - var mounted_node_id = edit.root; - var scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - var element = this.nodes[edit.root]; - element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + Interpreter.prototype.NewEventListener = function (event_name, scope, root) { + console.log('new event listener', event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } }; - Interpreter.prototype.SetText = function (edit) { - this.nodes[edit.root].textContent = edit.text; + Interpreter.prototype.RemoveEventListener = function (root, event_name, scope) { + // }; - Interpreter.prototype.SetAttribute = function (edit) { - // console.log("setting attr", edit); - var name = edit.field; - var value = edit.value; - var ns = edit.ns; - var node = this.nodes[edit.root]; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var node = this.nodes[root]; if (ns == "style") { // @ts-ignore node.style[name] = value; @@ -359,9 +355,8 @@ var Interpreter = /** @class */ (function () { } } }; - Interpreter.prototype.RemoveAttribute = function (edit) { - var name = edit.name; - var node = this.nodes[edit.root]; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { node.value = ""; @@ -376,81 +371,85 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.handleEdits = function (edits) { console.log("handling edits ", edits); this.stack.push(this.root); - var _loop_1 = function (edit) { - switch (edit.type) { - case "AppendChildren": - this_1.AppendChildren(edit); - break; - case "ReplaceWith": - this_1.ReplaceWith(edit); - break; - case "InsertAfter": - this_1.InsertAfter(edit); - break; - case "InsertBefore": - this_1.InsertBefore(edit); - break; - case "Remove": - this_1.Remove(edit); - break; - case "CreateTextNode": - this_1.CreateTextNode(edit); - break; - case "CreateElement": - this_1.CreateElement(edit); - break; - case "CreateElementNs": - this_1.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this_1.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this_1.RemoveEventListener(edit); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute("dioxus-prevent-default"); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents - }); - } - }; - this_1.NewEventListener(edit, handler); - break; - case "SetText": - this_1.SetText(edit); - break; - case "SetAttribute": - this_1.SetAttribute(edit); - break; - case "RemoveAttribute": - this_1.RemoveAttribute(edit); - break; - } - }; - var this_1 = this; for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; - _loop_1(edit); + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (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, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; } }; return Interpreter; }()); +exports.Interpreter = Interpreter; function main() { var root = window.document.getElementById("main"); if (root != null) { diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index faf9e1dab..e3547effb 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -318,7 +318,7 @@ const bool_attrs = { truespeed: true, }; -class Interpreter { +export class Interpreter { root: Element; stack: Element[]; listeners: { [key: string]: (event: Event) => void }; @@ -343,106 +343,102 @@ class Interpreter { return this.stack.pop(); } - PushRoot(edit: PushRoot) { - const id = edit.root; - const node = this.nodes[id]; + PushRoot(root: number) { + const node = this.nodes[root]; this.stack.push(node); } - AppendChildren(edit: AppendChildren) { - let root = this.stack[this.stack.length - (1 + edit.many)]; + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - edit.many); + let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < edit.many; i++) { + for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); } } - ReplaceWith(edit: ReplaceWith) { - let root = this.nodes[edit.root] as Element; - let els = this.stack.splice(this.stack.length - edit.m); + 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(edit: InsertAfter) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + 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(edit: InsertBefore) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + 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(edit: Remove) { - let node = this.nodes[edit.root] as Element; + Remove(root: number) { + let node = this.nodes[root] as Element; if (node !== undefined) { node.remove(); } } - CreateTextNode(edit: CreateTextNode) { + CreateTextNode(text: string, root: number) { // todo: make it so the types are okay - const node = document.createTextNode(edit.text) as any as Element; - this.nodes[edit.root] = node; + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; this.stack.push(node); } - CreateElement(edit: CreateElement) { - const el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", `${edit.root}`); + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + el.setAttribute("dioxus-id", `${root}`); - this.nodes[edit.root] = el; + this.nodes[root] = el; this.stack.push(el); } - CreateElementNs(edit: CreateElementNs) { - let el = document.createElementNS(edit.ns, edit.tag); + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - CreatePlaceholder(edit: CreatePlaceholder) { + CreatePlaceholder(root: number) { let el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - RemoveEventListener(edit: RemoveEventListener) { } - - NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - - const element = this.nodes[edit.root]; + NewEventListener(event_name: string, scope: number, root: number) { + console.log('new event listener', event_name, root, scope); + const element = this.nodes[root]; element.setAttribute( `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` + `${scope}.${root}` ); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } } - SetText(edit: SetText) { - this.nodes[edit.root].textContent = edit.text; + + RemoveEventListener(root: number, event_name: string, scope: number) { + // } - SetAttribute(edit: SetAttribute) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; + + 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") { @@ -477,10 +473,9 @@ class Interpreter { } } } - RemoveAttribute(edit: RemoveAttribute) { - const name = edit.name; + RemoveAttribute(root: number, name: string) { - const node = this.nodes[edit.root]; + const node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { @@ -501,79 +496,87 @@ class Interpreter { this.stack.push(this.root); for (let edit of edits) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit); - break; - case "ReplaceWith": - this.ReplaceWith(edit); - break; - case "InsertAfter": - this.InsertAfter(edit); - break; - case "InsertBefore": - this.InsertBefore(edit); - break; - case "Remove": - this.Remove(edit); - break; - case "CreateTextNode": - this.CreateTextNode(edit); - break; - case "CreateElement": - this.CreateElement(edit); - break; - case "CreateElementNs": - this.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - let handler = (event: Event) => { - const target = event.target as Element | null; - console.log("event", event); - if (target != null) { + this.handleEdit(edit); + } + } - const real_id = target.getAttribute(`dioxus-id`); + 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, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const real_id = target.getAttribute(`dioxus-id`); - let contents = serialize_event(event); + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } + let contents = serialize_event(event); - if (real_id == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); } - }; - this.NewEventListener(edit, handler); - break; - case "SetText": - this.SetText(edit); - break; - case "SetAttribute": - this.SetAttribute(edit); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit); - break; - } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; } } } @@ -632,5 +635,3 @@ declare global { type Edits = DomEdit[]; - -main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 698781908..8f39daddf 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "es5", - // "module": "None", "module": "commonjs", "strict": true, "outDir": "dist", diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 868c5d7b0..52a78e631 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} -dioxus-html = { path = "../html", version ="^0.1.4"} +dioxus-core = { path = "../core", version = "^0.1.7" } +dioxus-html = { path = "../html", version = "^0.1.4" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs new file mode 100644 index 000000000..d65fc1300 --- /dev/null +++ b/packages/web/src/bindings.rs @@ -0,0 +1,59 @@ +use dioxus_core::DomEdit; +use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; +use web_sys::{Element, HtmlElement, Node}; + +#[wasm_bindgen(module = "/src/interpreter.js")] +extern "C" { + pub type Interpreter; + + #[wasm_bindgen(constructor)] + pub fn new(arg: Element) -> Interpreter; + + #[wasm_bindgen(method)] + pub fn set_node(this: &Interpreter, id: usize, node: Node); + + #[wasm_bindgen(method)] + pub fn PushRoot(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, many: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn Remove(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); + + #[wasm_bindgen(method)] + pub fn CreatePlaceholder(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + + #[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 364f1a1d7..918af4c4a 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,10 +7,11 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> +use crate::bindings::Interpreter; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; use fxhash::FxHashMap; use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{ CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlTextAreaElement, Node, @@ -19,13 +20,10 @@ use web_sys::{ use crate::{nodeslab::NodeSlab, WebConfig}; pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - document: Document, + pub interpreter: Interpreter, + pub(crate) root: Element, sender_callback: Rc, @@ -42,7 +40,6 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); - let nodes = NodeSlab::new(2000); let listeners = FxHashMap::default(); let mut stack = Stack::with_capacity(10); @@ -52,8 +49,7 @@ impl WebsysDom { stack.push(root_node); Self { - stack, - nodes, + interpreter: Interpreter::new(root.clone()), listeners, document, sender_callback, @@ -64,399 +60,38 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { for edit in edits.drain(..) { match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) + DomEdit::PushRoot { root } => self.interpreter.PushRoot(root), + DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many), + DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m), + 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::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), + DomEdit::CreateElementNs { tag, root, ns } => { + self.interpreter.CreateElementNs(tag, root, ns) + } + DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), DomEdit::NewEventListener { event_name, scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) + root, + } => self.interpreter.NewEventListener(event_name, scope.0, root), + DomEdit::RemoveEventListener { root, event } => { + self.interpreter.RemoveEventListener(root, event) } - - DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), DomEdit::SetAttribute { + root, field, value, ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); + } => self.interpreter.SetAttribute(root, field, value, ns), + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.stack.top(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } - } - } - } - } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(&anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); } } } diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js new file mode 100644 index 000000000..1d0f14c68 --- /dev/null +++ b/packages/web/src/interpreter.js @@ -0,0 +1,529 @@ +"use strict"; +var exports = {}; +exports.__esModule = true; +exports.Interpreter = void 0; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + case "compositionend": + case "compositionstart": + case "compositionupdate": + var data = event.data; + return { + data: data, + }; + case "keydown": + case "keypress": + case "keyup": + var _c = event, + charCode = _c.charCode, + key = _c.key, + altKey = _c.altKey, + ctrlKey = _c.ctrlKey, + metaKey = _c.metaKey, + keyCode = _c.keyCode, + shiftKey = _c.shiftKey, + location_1 = _c.location, + repeat = _c.repeat, + which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale", + }; + case "focus": + case "blur": + return {}; + case "change": + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = + (_a = target.value) !== null && _a !== void 0 + ? _a + : target.textContent; + } + return { + value: value, + }; + case "input": + case "invalid": + case "reset": + case "submit": { + var target_1 = event.target; + var value_1 = + (_b = target_1.value) !== null && _b !== void 0 + ? _b + : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1, + }; + } + 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": { + var _d = event, + altKey_1 = _d.altKey, + button = _d.button, + buttons = _d.buttons, + clientX = _d.clientX, + clientY = _d.clientY, + ctrlKey_1 = _d.ctrlKey, + metaKey_1 = _d.metaKey, + pageX = _d.pageX, + pageY = _d.pageY, + screenX_1 = _d.screenX, + screenY_1 = _d.screenY, + shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, + altKey_2 = _e.altKey, + button = _e.button, + buttons = _e.buttons, + clientX = _e.clientX, + clientY = _e.clientY, + ctrlKey_2 = _e.ctrlKey, + metaKey_2 = _e.metaKey, + pageX = _e.pageX, + pageY = _e.pageY, + screenX_2 = _e.screenX, + screenY_2 = _e.screenY, + shiftKey_2 = _e.shiftKey, + pointerId = _e.pointerId, + width = _e.width, + height = _e.height, + pressure = _e.pressure, + tangentialPressure = _e.tangentialPressure, + tiltX = _e.tiltX, + tiltY = _e.tiltY, + twist = _e.twist, + pointerType = _e.pointerType, + isPrimary = _e.isPrimary; + return { + alt_key: altKey_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + 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": { + var _f = event, + altKey_3 = _f.altKey, + ctrlKey_3 = _f.ctrlKey, + metaKey_3 = _f.metaKey, + shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3, + }; + } + case "scroll": + return {}; + case "wheel": { + var _g = event, + deltaX = _g.deltaX, + deltaY = _g.deltaY, + deltaZ = _g.deltaZ, + deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, + animationName = _h.animationName, + elapsedTime = _h.elapsedTime, + pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + var _j = event, + propertyName = _j.propertyName, + elapsedTime = _j.elapsedTime, + pseudoElement = _j.pseudoElement; + 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 {}; + } +} +var 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, +}; +export var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (text, root) { + // todo: make it so the types are okay + var node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (root) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.NewEventListener = function (event_name, scope, root) { + console.log("new event listener", event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute( + "dioxus-event-".concat(event_name), + "".concat(scope, ".").concat(root) + ); + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } + }; + Interpreter.prototype.RemoveEventListener = function ( + root, + event_name, + scope + ) { + // + }; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var 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); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + console.log("handling edits ", edits); + this.stack.push(this.root); + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (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, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute( + "dioxus-prevent-default" + ); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; + } + }; + return Interpreter; +})(); +exports.Interpreter = Interpreter; +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 67d52f4fe..ba9398276 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,6 +61,7 @@ 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; diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs new file mode 100644 index 000000000..364f1a1d7 --- /dev/null +++ b/packages/web/src/olddom.rs @@ -0,0 +1,789 @@ +//! Implementation of a renderer for Dioxus on the web. +//! +//! Oustanding todos: +//! - Removing event listeners (delegation) +//! - Passive event listeners +//! - no-op event listener patch for safari +//! - tests to ensure dyn_into works for various event types. +//! - Partial delegation?> + +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; +use fxhash::FxHashMap; +use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, HtmlTextAreaElement, Node, +}; + +use crate::{nodeslab::NodeSlab, WebConfig}; + +pub struct WebsysDom { + stack: Stack, + + /// A map from ElementID (index) to Node + pub(crate) nodes: NodeSlab, + + document: Document, + + pub(crate) root: Element, + + sender_callback: Rc, + + // map of listener types to number of those listeners + // This is roughly a delegater + // TODO: check how infero delegates its events - some are more performant + listeners: FxHashMap<&'static str, ListenerEntry>, +} + +type ListenerEntry = (usize, Closure); + +impl WebsysDom { + pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { + let document = load_document(); + + let nodes = NodeSlab::new(2000); + let listeners = FxHashMap::default(); + + let mut stack = Stack::with_capacity(10); + + let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let root_node = root.clone().dyn_into::().unwrap(); + stack.push(root_node); + + Self { + stack, + nodes, + listeners, + document, + sender_callback, + root, + } + } + + pub fn apply_edits(&mut self, mut edits: Vec) { + for edit in edits.drain(..) { + match edit { + DomEdit::PushRoot { root } => self.push(root), + DomEdit::AppendChildren { many } => self.append_children(many), + DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), + DomEdit::Remove { root } => self.remove(root), + DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), + DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), + DomEdit::CreateElementNs { tag, root: id, ns } => { + self.create_element(tag, Some(ns), id) + } + DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::NewEventListener { + event_name, + scope, + root: mounted_node_id, + } => self.new_event_listener(event_name, scope, mounted_node_id), + + DomEdit::RemoveEventListener { event, root } => { + self.remove_event_listener(event, root) + } + + DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetAttribute { + field, + value, + ns, + root, + } => self.set_attribute(field, value, ns, root), + DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), + + DomEdit::InsertAfter { n, root } => self.insert_after(n, root), + DomEdit::InsertBefore { n, root } => self.insert_before(n, root), + } + } + } + fn push(&mut self, root: u64) { + let key = root as usize; + let domnode = &self.nodes[key]; + + let real_node: Node = match domnode { + Some(n) => n.clone(), + None => todo!(), + }; + + self.stack.push(real_node); + } + + fn append_children(&mut self, many: u32) { + let root: Node = self + .stack + .list + .get(self.stack.list.len() - (1 + many as usize)) + .unwrap() + .clone(); + + // We need to make sure to add comments between text nodes + // We ensure that the text siblings are patched by preventing the browser from merging + // neighboring text nodes. Originally inspired by some of React's work from 2016. + // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes + // -> https://github.com/facebook/react/pull/5753 + /* + todo: we need to track this for replacing/insert after/etc + */ + let mut last_node_was_text = false; + + for child in self + .stack + .list + .drain((self.stack.list.len() - many as usize)..) + { + if child.dyn_ref::().is_some() { + if last_node_was_text { + let comment_node = self + .document + .create_comment("dioxus") + .dyn_into::() + .unwrap(); + root.append_child(&comment_node).unwrap(); + } + last_node_was_text = true; + } else { + last_node_was_text = false; + } + root.append_child(&child).unwrap(); + } + } + + fn replace_with(&mut self, m: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - m as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } + } + + fn remove(&mut self, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(element) = node.dyn_ref::() { + element.remove(); + } else { + if let Some(parent) = node.parent_node() { + parent.remove_child(&node).unwrap(); + } + } + } + + fn create_placeholder(&mut self, id: u64) { + self.create_element("pre", None, id); + self.set_attribute("hidden", "", None, id); + } + + fn create_text_node(&mut self, text: &str, id: u64) { + let textnode = self + .document + .create_text_node(text) + .dyn_into::() + .unwrap(); + + self.stack.push(textnode.clone()); + + self.nodes[(id as usize)] = Some(textnode); + } + + fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { + let tag = wasm_bindgen::intern(tag); + + let el = match ns { + Some(ns) => self + .document + .create_element_ns(Some(ns), tag) + .unwrap() + .dyn_into::() + .unwrap(), + None => self + .document + .create_element(tag) + .unwrap() + .dyn_into::() + .unwrap(), + }; + + use smallstr::SmallString; + use std::fmt::Write; + + let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); + write!(s, "{}", id).unwrap(); + + let el2 = el.dyn_ref::().unwrap(); + el2.set_attribute("dioxus-id", s.as_str()).unwrap(); + + self.stack.push(el.clone()); + self.nodes[(id as usize)] = Some(el); + } + + fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + let event = wasm_bindgen::intern(event); + + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. + + let el = self.stack.top(); + + let el = el.dyn_ref::().unwrap(); + + el.set_attribute("dioxus-event", event).unwrap(); + + // Register the callback to decode + + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.sender_callback.clone(); + + let c: Box = Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + match decode_trigger(event) { + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), + }; + }); + + let handler = Closure::wrap(c); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } + } + + fn remove_event_listener(&mut self, _event: &str, _root: u64) { + todo!() + } + + fn set_text(&mut self, text: &str, root: u64) { + let el = self.nodes[root as usize].as_ref().unwrap(); + el.set_text_content(Some(text)) + } + + fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if ns == Some("style") { + if let Some(el) = node.dyn_ref::() { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); + } + } else { + let fallback = || { + let el = node.dyn_ref::().unwrap(); + el.set_attribute(name, value).unwrap() + }; + match name { + "dangerous_inner_html" => { + if let Some(el) = node.dyn_ref::() { + el.set_inner_html(value); + } + } + "value" => { + if let Some(input) = node.dyn_ref::() { + /* + if the attribute being set is the same as the value of the input, then don't bother setting it. + This is used in controlled components to keep the cursor in the right spot. + + this logic should be moved into the virtualdom since we have the notion of "volatile" + */ + if input.value() != value { + input.set_value(value); + } + } else if let Some(node) = node.dyn_ref::() { + if name == "value" { + node.set_value(value); + } + } else { + fallback(); + } + } + "checked" => { + if let Some(input) = node.dyn_ref::() { + match value { + "true" => input.set_checked(true), + "false" => input.set_checked(false), + _ => fallback(), + } + } else { + fallback(); + } + } + "selected" => { + if let Some(node) = node.dyn_ref::() { + node.set_selected(true); + } else { + fallback(); + } + } + _ => { + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if value == "false" { + if let Some(el) = node.dyn_ref::() { + match name { + "allowfullscreen" + | "allowpaymentrequest" + | "async" + | "autofocus" + | "autoplay" + | "checked" + | "controls" + | "default" + | "defer" + | "disabled" + | "formnovalidate" + | "hidden" + | "ismap" + | "itemscope" + | "loop" + | "multiple" + | "muted" + | "nomodule" + | "novalidate" + | "open" + | "playsinline" + | "readonly" + | "required" + | "reversed" + | "selected" + | "truespeed" => { + let _ = el.remove_attribute(name); + } + _ => { + let _ = el.set_attribute(name, value); + } + }; + } + } else { + fallback(); + } + } + } + } + } + + fn remove_attribute(&mut self, name: &str, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + // Some attributes are "volatile" and don't work through `removeAttribute`. + if name == "value" { + node.set_value(""); + } + if name == "checked" { + node.set_checked(false); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); + } + } + } + + fn insert_after(&mut self, n: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } + } + + fn insert_before(&mut self, n: u32, root: u64) { + let anchor = self.nodes[root as usize].as_ref().unwrap(); + + if n == 1 { + let before = self.stack.pop(); + + anchor + .parent_node() + .unwrap() + .insert_before(&before, Some(&anchor)) + .unwrap(); + } else { + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } + } + } +} + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +pub struct DioxusWebsysEvent(web_sys::Event); + +// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +unsafe impl Send for DioxusWebsysEvent {} +unsafe impl Sync for DioxusWebsysEvent {} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { + use dioxus_html::on::*; + use dioxus_html::KeyCode; + + match event.type_().as_str() { + "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Arc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) + } + "keydown" | "keypress" | "keyup" => { + let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); + Arc::new(KeyboardData { + alt_key: evt.alt_key(), + char_code: evt.char_code(), + key: evt.key(), + key_code: KeyCode::from_raw_code(evt.key_code() as u8), + ctrl_key: evt.ctrl_key(), + locale: "not implemented".to_string(), + location: evt.location() as usize, + meta_key: evt.meta_key(), + repeat: evt.repeat(), + shift_key: evt.shift_key(), + which: evt.which() as usize, + }) + } + "focus" | "blur" => Arc::new(FocusData {}), + + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + "change" | "input" | "invalid" | "reset" | "submit" => { + let evt: &web_sys::Event = event.dyn_ref().unwrap(); + + let target: web_sys::EventTarget = evt.target().unwrap(); + let value: String = (&target) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + Arc::new(FormData { value }) + } + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); + Arc::new(MouseData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + }) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); + Arc::new(PointerData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + pointer_id: evt.pointer_id(), + width: evt.width(), + height: evt.height(), + pressure: evt.pressure(), + tangential_pressure: evt.tangential_pressure(), + tilt_x: evt.tilt_x(), + tilt_y: evt.tilt_y(), + twist: evt.twist(), + pointer_type: evt.pointer_type(), + is_primary: evt.is_primary(), + // get_modifier_state: evt.get_modifier_state(), + }) + } + "select" => Arc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); + Arc::new(TouchData { + alt_key: evt.alt_key(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + shift_key: evt.shift_key(), + }) + } + "scroll" => Arc::new(()), + "wheel" => { + let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); + Arc::new(WheelData { + delta_x: evt.delta_x(), + delta_y: evt.delta_y(), + delta_z: evt.delta_z(), + delta_mode: evt.delta_mode(), + }) + } + "animationstart" | "animationend" | "animationiteration" => { + let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); + Arc::new(AnimationData { + elapsed_time: evt.elapsed_time(), + animation_name: evt.animation_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "transitionend" => { + let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); + Arc::new(TransitionData { + elapsed_time: evt.elapsed_time(), + property_name: evt.property_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), + "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), + } +} + +/// This function decodes a websys event and produces an EventTrigger +/// With the websys implementation, we attach a unique key to the nodes +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { + use anyhow::Context; + + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let element_id = target + .get_attribute("dioxus-id") + .context("Could not find element id on event target")? + .parse()?; + + Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(element_id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +fn event_name_from_typ(typ: &str) -> &'static str { + match typ { + "copy" => "copy", + "cut" => "cut", + "paste" => "paste", + "compositionend" => "compositionend", + "compositionstart" => "compositionstart", + "compositionupdate" => "compositionupdate", + "keydown" => "keydown", + "keypress" => "keypress", + "keyup" => "keyup", + "focus" => "focus", + "blur" => "blur", + "change" => "change", + "input" => "input", + "invalid" => "invalid", + "reset" => "reset", + "submit" => "submit", + "click" => "click", + "contextmenu" => "contextmenu", + "doubleclick" => "doubleclick", + "drag" => "drag", + "dragend" => "dragend", + "dragenter" => "dragenter", + "dragexit" => "dragexit", + "dragleave" => "dragleave", + "dragover" => "dragover", + "dragstart" => "dragstart", + "drop" => "drop", + "mousedown" => "mousedown", + "mouseenter" => "mouseenter", + "mouseleave" => "mouseleave", + "mousemove" => "mousemove", + "mouseout" => "mouseout", + "mouseover" => "mouseover", + "mouseup" => "mouseup", + "pointerdown" => "pointerdown", + "pointermove" => "pointermove", + "pointerup" => "pointerup", + "pointercancel" => "pointercancel", + "gotpointercapture" => "gotpointercapture", + "lostpointercapture" => "lostpointercapture", + "pointerenter" => "pointerenter", + "pointerleave" => "pointerleave", + "pointerover" => "pointerover", + "pointerout" => "pointerout", + "select" => "select", + "touchcancel" => "touchcancel", + "touchend" => "touchend", + "touchmove" => "touchmove", + "touchstart" => "touchstart", + "scroll" => "scroll", + "wheel" => "wheel", + "animationstart" => "animationstart", + "animationend" => "animationend", + "animationiteration" => "animationiteration", + "transitionend" => "transitionend", + "abort" => "abort", + "canplay" => "canplay", + "canplaythrough" => "canplaythrough", + "durationchange" => "durationchange", + "emptied" => "emptied", + "encrypted" => "encrypted", + "ended" => "ended", + "error" => "error", + "loadeddata" => "loadeddata", + "loadedmetadata" => "loadedmetadata", + "loadstart" => "loadstart", + "pause" => "pause", + "play" => "play", + "playing" => "playing", + "progress" => "progress", + "ratechange" => "ratechange", + "seeked" => "seeked", + "seeking" => "seeking", + "stalled" => "stalled", + "suspend" => "suspend", + "timeupdate" => "timeupdate", + "volumechange" => "volumechange", + "waiting" => "waiting", + "toggle" => "toggle", + _ => { + panic!("unsupported event type") + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..25b126036 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -80,7 +80,8 @@ impl WebsysDom { *last_node_was_text = true; - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } @@ -105,7 +106,8 @@ impl WebsysDom { .set_attribute("dioxus-id", s.as_str()) .unwrap(); - self.nodes[node_id.0] = Some(node.clone()); + self.interpreter.set_node(node_id.0, node.clone()); + // self.nodes[node_id.0] = Some(node.clone()); *cur_place += 1; @@ -135,7 +137,9 @@ impl WebsysDom { let cur_place = place.last_mut().unwrap(); let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } From e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:44:53 -0600 Subject: [PATCH 005/244] Add title prop to Link Every element can have a title, but it's particularly useful on links, so I think making it an explicit option is worthwhile. --- packages/router/src/components/link.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 634696bf4..dc807aa1a 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -31,6 +31,9 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] id: Option<&'a str>, + #[props(default, strip_option)] + title: Option<&'a str>, + children: Element<'a>, #[props(default)] @@ -44,6 +47,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { 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), From d367e0f89f1b31484bde42efaf10c3531aed1d64 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:53:45 -0600 Subject: [PATCH 006/244] Fix typo in RouterService struct's "registered_routes" field name This was "registerd_routes", missing an "e". --- packages/router/src/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7f069b6ba..ae7c1ff05 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -10,7 +10,7 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registerd_routes: RefCell, + registered_routes: RefCell, slots: Rc>>, root_found: Rc>, cur_root: RefCell, @@ -53,7 +53,7 @@ impl RouterService { }); Self { - registerd_routes: RefCell::new(RouteSlot::Routes { + registered_routes: RefCell::new(RouteSlot::Routes { partial: String::from("/"), total: String::from("/"), rest: Vec::new(), From 3a5b417ad1639a31e9aac9f41b05d8b3f074b128 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:55:13 -0600 Subject: [PATCH 007/244] Add more trace messages to the RouterService code I was trying to debug some issues with my routes and this additional tracing was quite helpful. --- packages/router/src/components/route.rs | 1 + packages/router/src/service.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 2d9a270b8..129953eea 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,6 +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); // provide our route context let route_context = cx.provide_context(RouteContext { diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index ae7c1ff05..7eddeb69d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -46,8 +46,8 @@ impl RouterService { let listener = history.listen(move || { _root_found.set(false); // checking if the route is valid is cheap, so we do it - for (slot, _) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?}", slot); + for (slot, root) in _slots.borrow_mut().iter().rev() { + log::trace!("regenerating slot {:?} for root '{}'", slot, root); regen(*slot); } }); @@ -68,20 +68,25 @@ 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) { + log::trace!("Registered route '{}' with scope id {:?}", route, scope); self.slots.borrow_mut().push((scope, route)); } pub fn should_render(&self, scope: ScopeId) -> bool { + log::trace!("Should render scope id {:?}?", scope); if self.root_found.get() { + log::trace!(" no - because root_found is true"); return false; } let location = self.history.borrow().location(); let path = location.path(); + log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -90,14 +95,22 @@ impl RouterService { // fallback logic match root { Some((_id, route)) => { + log::trace!( + " matched given scope id {:?} with route root '{}'", + scope, + route, + ); if route == path { + log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); 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(true); true } else { + log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } From 3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sun, 9 Jan 2022 23:44:37 -0600 Subject: [PATCH 008/244] Commit WIP on router There are a few different changes in here that probably need to be picked apart. I'm sure much of this is wrong. * Fix missing `dyn` that compiler complained about in router.rs * Make UseRoute store a `Rc` rather than a string so we can get information out of the router like current location. * Implement `UseRoute`'s nth_segment and last_segment methods. I changed the return type to a String because of the above. * Remove some unused imports in platform/mod.rs and service.rs * Implement the `use_route` fn. It panics if called outside a Router { } (I think). I think that makes sense. * Add a `current_location` method to `RouterService` that returns the current location. I needed this both for the `UseRoute` implementation and _also_ so I could get at this in my webapp code. I think having some way to get this will be useful for others, whether or not this exact API is used. In my case, I want to compare the current path to the `to` path of a `Link` so I can use a different class for that `Link` if it is the currently active page. --- packages/router/src/components/router.rs | 2 +- packages/router/src/hooks/use_route.rs | 40 ++++++++++++++++++++---- packages/router/src/platform/mod.rs | 2 -- packages/router/src/service.rs | 7 +++-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 85f0a62a3..3d9f0c83a 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -12,7 +12,7 @@ pub struct RouterProps<'a> { children: Element<'a>, #[props(default, strip_option)] - onchange: Option<&'a Fn(&'a str)>, + onchange: Option<&'a dyn Fn(&'a str)>, } #[allow(non_snake_case)] diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 8307ed1f0..bf76d0975 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,7 +1,11 @@ use dioxus_core::ScopeState; +use gloo::history::Location; +use std::rc::Rc; + +use crate::RouterService; pub struct UseRoute<'a> { - cur_route: String, + router: Rc, cx: &'a ScopeState, } @@ -11,20 +15,44 @@ impl<'a> UseRoute<'a> { todo!() } - pub fn nth_segment(&self, n: usize) -> Option<&str> { - todo!() + pub fn nth_segment(&self, n: usize) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len - 1 < n { + return None; + } + Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option<&'a str> { - todo!() + pub fn last_segment(&self) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len == 0 { + return None; + } + Some(segments.remove(len - 1)) } /// Parse the segments of the URL, using named parameters (defined in your router) pub fn segment(&self, name: &str) -> Option<&T> { todo!() } + + pub fn current_location(&self) -> Location { + self.router.current_location() + } + + fn path_segments(&self) -> Vec { + let location = self.router.current_location(); + let stripped = &location.path()[1..]; + stripped.split('/').map(str::to_string).collect::>() + } } pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { - todo!() + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + .clone(); + UseRoute { router, cx } } diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index bbb0e7c18..7afc1316a 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,5 +1,3 @@ -use url::Url; - pub trait RouterProvider { fn get_current_route(&self) -> String; fn subscribe_to_route_changes(&self, callback: Box); diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7eddeb69d..c0b383e81 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,7 +1,6 @@ -use gloo::history::{BrowserHistory, History, HistoryListener}; +use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, RefCell}, - collections::HashMap, rc::Rc, }; @@ -118,6 +117,10 @@ impl RouterService { None => false, } } + + pub fn current_location(&self) -> Location { + self.history.borrow().location().clone() + } } pub struct RouterCfg { From f8a7e1cd8255fd7d0116384247e0e305bb73bf3d Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Mon, 10 Jan 2022 11:30:26 -0600 Subject: [PATCH 009/244] Implement router matching for path parameters We don't want to have the router just always match paths as exact strings. If a path contains a parameter like "/thing/:id" then the ":id" portion of the route should match _any_ string, not a literal ":id". --- packages/router/src/service.rs | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index c0b383e81..9525ac44c 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -72,8 +72,9 @@ impl RouterService { } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { - log::trace!("Registered route '{}' with scope id {:?}", route, scope); - self.slots.borrow_mut().push((scope, route)); + let clean = clean_route(route); + log::trace!("Registered route '{}' with scope id {:?}", clean, scope); + self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { @@ -99,7 +100,7 @@ impl RouterService { scope, route, ); - if route == path { + if route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); true @@ -123,6 +124,59 @@ impl RouterService { } } +fn clean_route(route: String) -> String { + if route.as_str() == "/" { + return route; + } + route.trim_end_matches('/').to_string() +} + +fn clean_path(path: &str) -> &str { + if path == "/" { + return path; + } + path.trim_end_matches('/') +} + +fn route_matches_path(route: &str, path: &str) -> bool { + 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 false; + } + + 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, + ); + continue; + } + log::trace!( + " route piece '{}' must be an exact match for path piece '{}'", + r, + path_pieces[i], + ); + if path_pieces[i] != *r { + return false; + } + } + + return true; +} + pub struct RouterCfg { initial_route: String, } From c9408da7310423b1676fab6d41635f9a8000d89e Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 10:47:17 -0600 Subject: [PATCH 010/244] Implement UseRoute segment method This relies on the RouterService to capture path params when it does path matching. --- packages/router/src/hooks/use_route.rs | 17 ++++++++++---- packages/router/src/service.rs | 31 ++++++++++++++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index bf76d0975..5ed056f65 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,6 +1,6 @@ use dioxus_core::ScopeState; use gloo::history::Location; -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use crate::RouterService; @@ -33,9 +33,18 @@ impl<'a> UseRoute<'a> { Some(segments.remove(len - 1)) } - /// Parse the segments of the URL, using named parameters (defined in your router) - pub fn segment(&self, name: &str) -> Option<&T> { - todo!() + /// Get the named parameter from the path, as defined in your router. The + /// value will be parsed into the type specified by `T` by calling + /// `value.parse::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn segment(&self, name: &str) -> Option> + where + T: FromStr, + { + self.router + .current_path_params() + .get(name) + .and_then(|v| Some(v.parse::())) } pub fn current_location(&self) -> Location { diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 9525ac44c..2fac8509b 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,6 +1,7 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ - cell::{Cell, RefCell}, + cell::{Cell, Ref, RefCell}, + collections::HashMap, rc::Rc, }; @@ -9,10 +10,9 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registered_routes: RefCell, slots: Rc>>, root_found: Rc>, - cur_root: RefCell, + cur_path_params: Rc>>, listener: HistoryListener, } @@ -52,16 +52,11 @@ impl RouterService { }); Self { - registered_routes: RefCell::new(RouteSlot::Routes { - partial: String::from("/"), - total: String::from("/"), - rest: Vec::new(), - }), root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, - cur_root: RefCell::new(path.to_string()), + cur_path_params: Rc::new(RefCell::new(HashMap::new())), listener, } } @@ -100,9 +95,10 @@ impl RouterService { scope, route, ); - if route_matches_path(route, path) { + if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); + *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { @@ -122,6 +118,10 @@ impl RouterService { pub fn current_location(&self) -> Location { self.history.borrow().location().clone() } + + pub fn current_path_params(&self) -> Ref> { + self.cur_path_params.borrow() + } } fn clean_route(route: String) -> String { @@ -138,7 +138,7 @@ fn clean_path(path: &str) -> &str { path.trim_end_matches('/') } -fn route_matches_path(route: &str, path: &str) -> bool { +fn route_matches_path(route: &str, path: &str) -> Option> { let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); @@ -150,9 +150,10 @@ fn route_matches_path(route: &str, path: &str) -> bool { if route_pieces.len() != path_pieces.len() { log::trace!(" the routes are different lengths"); - return false; + 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 @@ -162,6 +163,8 @@ fn route_matches_path(route: &str, path: &str) -> bool { " 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!( @@ -170,11 +173,11 @@ fn route_matches_path(route: &str, path: &str) -> bool { path_pieces[i], ); if path_pieces[i] != *r { - return false; + return None; } } - return true; + Some(matches) } pub struct RouterCfg { From e06eac1ce5c6bb0a6680482574d82b16a141a626 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 11:27:02 -0600 Subject: [PATCH 011/244] More WIP router implementation --- examples/router.rs | 12 +++++-- packages/router/src/hooks/use_route.rs | 46 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index bd6d51881..121514423 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use dioxus::router::{Link, Route, Router}; +use serde::Deserialize; fn main() { dioxus::desktop::launch(app); @@ -30,7 +31,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; + let post = dioxus::router::use_route(&cx).last_segment(); cx.render(rsx! { div { @@ -40,9 +41,14 @@ fn BlogPost(cx: Scope) -> Element { }) } +#[derive(Deserialize)] +struct Query { + bold: bool, +} + fn User(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; - let bold = dioxus::router::use_route(&cx).param::("bold"); + let post = dioxus::router::use_route(&cx).last_segment(); + let query = dioxus::router::use_route(&cx).query::(); cx.render(rsx! { div { diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 5ed056f65..97dd6c048 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,20 +1,29 @@ use dioxus_core::ScopeState; -use gloo::history::Location; +use gloo::history::{HistoryResult, Location}; +use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; use crate::RouterService; -pub struct UseRoute<'a> { +/// This struct provides is a wrapper around the internal router +/// implementation, with methods for getting information about the current +/// route. +pub struct UseRoute { router: Rc, - cx: &'a ScopeState, } -impl<'a> UseRoute<'a> { - /// Parse the query part of the URL - pub fn param(&self, param: &str) -> Option<&T> { - todo!() +impl UseRoute { + /// This method simply calls the [`Location::query`] method. + pub fn query(&self) -> HistoryResult + where + T: DeserializeOwned, + { + self.current_location().query::() } + /// Returns the nth segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. If the path has + /// fewer segments than `n` then this method returns `None`. pub fn nth_segment(&self, n: usize) -> Option { let mut segments = self.path_segments(); let len = segments.len(); @@ -24,13 +33,12 @@ impl<'a> UseRoute<'a> { Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option { + /// Returns the last segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. The root path, `/`, + /// will return an empty string. + pub fn last_segment(&self) -> String { let mut segments = self.path_segments(); - let len = segments.len(); - if len == 0 { - return None; - } - Some(segments.remove(len - 1)) + segments.remove(segments.len() - 1) } /// Get the named parameter from the path, as defined in your router. The @@ -47,21 +55,29 @@ impl<'a> UseRoute<'a> { .and_then(|v| Some(v.parse::())) } + /// Returns the [Location] for the current route. pub fn current_location(&self) -> Location { self.router.current_location() } fn path_segments(&self) -> Vec { let location = self.router.current_location(); + let path = location.path(); + if path == "/" { + return vec![String::new()]; + } let stripped = &location.path()[1..]; stripped.split('/').map(str::to_string).collect::>() } } -pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { +/// 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, cx } + UseRoute { router } } From 05331ddd8033f6997d4916179b62f4d62f832988 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 13 Jan 2022 22:51:01 -0500 Subject: [PATCH 012/244] feat: wire up both desktop and web --- packages/desktop/src/index.html | 1 + packages/jsinterpreter/README.md | 8 + packages/jsinterpreter/interpreter.js | 133 ++++--- packages/jsinterpreter/interpreter.ts | 77 ++-- packages/jsinterpreter/tsconfig.json | 14 +- packages/web/src/bindings.rs | 16 +- packages/web/src/cache.rs | 2 +- packages/web/src/cfg.rs | 5 +- packages/web/src/dom.rs | 94 ++--- packages/web/src/interpreter.js | 529 -------------------------- packages/web/src/lib.rs | 1 - packages/web/src/nodeslab.rs | 34 -- packages/web/src/olddom.rs | 68 ++++ packages/web/src/rehydrate.rs | 4 +- packages/web/src/ric_raf.rs | 3 +- 15 files changed, 261 insertions(+), 728 deletions(-) create mode 100644 packages/jsinterpreter/README.md delete mode 100644 packages/web/src/interpreter.js delete mode 100644 packages/web/src/nodeslab.rs diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 888ab8437..c17a63d2a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -15,5 +15,6 @@ + diff --git a/packages/jsinterpreter/README.md b/packages/jsinterpreter/README.md new file mode 100644 index 000000000..bab57ac16 --- /dev/null +++ b/packages/jsinterpreter/README.md @@ -0,0 +1,8 @@ +# JS Interpreter + +After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support. + +In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. + + +For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 6139280f8..453b34406 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,23 +1,22 @@ -"use strict"; -exports.__esModule = true; -exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { var data = event.data; return { - data: data + data: data, }; + } case "keydown": case "keypress": - case "keyup": + case "keyup": { var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; return { char_code: charCode, @@ -30,12 +29,14 @@ function serialize_event(event) { location: location_1, repeat: repeat, which: which, - locale: "locale" + locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; - case "change": + } + case "change": { var target = event.target; var value = void 0; if (target.type === "checkbox" || target.type === "radio") { @@ -45,19 +46,20 @@ function serialize_event(event) { value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; } return { - value: value + value: value, }; + } case "input": case "invalid": case "reset": case "submit": { - var target_1 = event.target; - var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; + var target = event.target; + var value = (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; } return { - value: value_1 + value: value, }; } case "click": @@ -78,20 +80,20 @@ function serialize_event(event) { case "mouseout": case "mouseover": case "mouseup": { - var _d = event, altKey_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey; return { - alt_key: altKey_1, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_1, screen_y: screenY_1, - shift_key: shiftKey_1 + shift_key: shiftKey, }; } case "pointerdown": @@ -104,20 +106,20 @@ function serialize_event(event) { case "pointerleave": case "pointerover": case "pointerout": { - var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; + var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; return { - alt_key: altKey_2, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_2, screen_y: screenY_2, - shift_key: shiftKey_2, + shift_key: shiftKey, pointer_id: pointerId, width: width, height: height, @@ -127,35 +129,37 @@ function serialize_event(event) { tilt_y: tiltY, twist: twist, pointer_type: pointerType, - is_primary: isPrimary + is_primary: isPrimary, }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": case "touchmove": case "touchstart": { - var _f = event, altKey_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey; return { // changed_touches: event.changedTouches, // target_touches: event.targetTouches, // touches: event.touches, - alt_key: altKey_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3 + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, }; } - case "scroll": + case "scroll": { return {}; + } case "wheel": { var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; return { delta_x: deltaX, delta_y: deltaY, delta_z: deltaZ, - delta_mode: deltaMode + delta_mode: deltaMode, }; } case "animationstart": @@ -165,7 +169,7 @@ function serialize_event(event) { return { animation_name: animationName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "transitionend": { @@ -173,7 +177,7 @@ function serialize_event(event) { return { property_name: propertyName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "abort": @@ -198,12 +202,15 @@ function serialize_event(event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; - case "toggle": + } + case "toggle": { return {}; - default: + } + default: { return {}; + } } } var bool_attrs = { @@ -214,7 +221,7 @@ var bool_attrs = { autoplay: true, checked: true, controls: true, - "default": true, + default: true, defer: true, disabled: true, formnovalidate: true, @@ -232,13 +239,14 @@ var bool_attrs = { required: true, reversed: true, selected: true, - truespeed: true + truespeed: true, }; var Interpreter = /** @class */ (function () { function Interpreter(root) { this.root = root; this.stack = [root]; this.listeners = {}; + this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; } @@ -303,17 +311,28 @@ var Interpreter = /** @class */ (function () { this.stack.push(el); this.nodes[root] = el; }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root) { - console.log('new event listener', event_name, root, scope); + Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) { + // console.log('new event listener', event_name, root, scope); var element = this.nodes[root]; element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + 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]++; + } }; - Interpreter.prototype.RemoveEventListener = function (root, event_name, scope) { - // + Interpreter.prototype.RemoveEventListener = function (root, event_name) { + var element = this.nodes[root]; + element.removeAttribute("dioxus-event-".concat(event_name)); + 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]; + } }; Interpreter.prototype.SetText = function (root, text) { this.nodes[root].textContent = text; @@ -369,7 +388,6 @@ var Interpreter = /** @class */ (function () { } }; Interpreter.prototype.handleEdits = function (edits) { - console.log("handling edits ", edits); this.stack.push(this.root); for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; @@ -409,13 +427,13 @@ var Interpreter = /** @class */ (function () { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": // todo: only on desktop should we make our own handler var handler = function (event) { var target = event.target; - console.log("event", event); + // console.log("event", event); if (target != null) { var real_id = target.getAttribute("dioxus-id"); var should_prevent_default = target.getAttribute("dioxus-prevent-default"); @@ -429,12 +447,11 @@ var Interpreter = /** @class */ (function () { window.rpc.call("user_event", { event: edit.event_name, mounted_dom_id: parseInt(real_id), - contents: contents + contents: contents, }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); @@ -449,7 +466,7 @@ var Interpreter = /** @class */ (function () { }; return Interpreter; }()); -exports.Interpreter = Interpreter; +export { Interpreter }; function main() { var root = window.document.getElementById("main"); if (root != null) { @@ -457,4 +474,4 @@ function main() { window.rpc.call("initialize"); } } -main(); +//# sourceMappingURL=interpreter.js.map \ No newline at end of file diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index e3547effb..17617a209 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,23 +1,23 @@ - - function serialize_event(event: Event) { switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { let { data } = (event as CompositionEvent); return { data, }; + } case "keydown": case "keypress": - case "keyup": + case "keyup": { let { charCode, key, @@ -44,12 +44,14 @@ function serialize_event(event: Event) { which: which, locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; + } - case "change": + case "change": { let target = event.target as HTMLInputElement; let value; if (target.type === "checkbox" || target.type === "radio") { @@ -61,6 +63,7 @@ function serialize_event(event: Event) { return { value: value, }; + } case "input": case "invalid": @@ -187,8 +190,9 @@ function serialize_event(event: Event) { }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": @@ -211,8 +215,10 @@ function serialize_event(event: Event) { }; } - case "scroll": + case "scroll": { return {}; + } + case "wheel": { const { deltaX, @@ -278,14 +284,17 @@ function serialize_event(event: Event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; + } - case "toggle": + case "toggle": { return {}; + } - default: + default: { return {}; + } } } @@ -321,7 +330,8 @@ const bool_attrs = { export class Interpreter { root: Element; stack: Element[]; - listeners: { [key: string]: (event: Event) => void }; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; lastNodeWasText: boolean; nodes: Element[]; @@ -329,8 +339,8 @@ export class Interpreter { constructor(root: Element) { this.root = root; this.stack = [root]; - this.listeners = { - }; + this.listeners = {}; + this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; } @@ -412,23 +422,36 @@ export class Interpreter { this.nodes[root] = el; } - NewEventListener(event_name: string, scope: number, root: number) { - console.log('new event listener', event_name, root, scope); + NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) { + // console.log('new event listener', event_name, root, scope); const element = this.nodes[root]; element.setAttribute( `dioxus-event-${event_name}`, `${scope}.${root}` ); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + 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( + `dioxus-event-${event_name}` + ); - RemoveEventListener(root: number, event_name: string, scope: number) { - // + 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]; + } } @@ -492,7 +515,6 @@ export class Interpreter { } handleEdits(edits: DomEdit[]) { - console.log("handling edits ", edits); this.stack.push(this.root); for (let edit of edits) { @@ -533,13 +555,13 @@ export class Interpreter { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": // todo: only on desktop should we make our own handler let handler = (event: Event) => { const target = event.target as Element | null; - console.log("event", event); + // console.log("event", event); if (target != null) { const real_id = target.getAttribute(`dioxus-id`); @@ -565,8 +587,7 @@ export class Interpreter { }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 8f39daddf..883760881 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,9 +1,13 @@ { "compilerOptions": { - "target": "es5", - "module": "commonjs", - "strict": true, - "outDir": "dist", - "sourceMap": true + "target": "ES5", + "module": "ESNext", + "lib": [ + "es2015", + "es5", + "es6", + "dom" + ], + "strict": true } } diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index d65fc1300..cc4202886 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -1,8 +1,8 @@ -use dioxus_core::DomEdit; -use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; -use web_sys::{Element, HtmlElement, Node}; +use js_sys::Function; +use wasm_bindgen::prelude::*; +use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/src/interpreter.js")] +#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] extern "C" { pub type Interpreter; @@ -43,7 +43,13 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + scope: usize, + root: u64, + handler: &Function, + ); #[wasm_bindgen(method)] pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index 190b505c1..e17203a65 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -1,4 +1,4 @@ -pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[ +pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ // Important tags to dioxus "dioxus-id", "dioxus", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 0fa0dc48c..cc5d18bf7 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -3,10 +3,9 @@ /// This struct helps configure the specifics of hydration and render destination for WebSys. /// /// # Example +/// /// ```rust, ignore -/// fn main() { -/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) -/// } +/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) /// ``` pub struct WebConfig { pub(crate) hydrate: bool, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 918af4c4a..c4bd6a6e5 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,51 +8,50 @@ //! - Partial delegation?> use crate::bindings::Interpreter; -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use fxhash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use js_sys::Function; +use std::{any::Any, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{Document, Element, Event, HtmlElement}; -use crate::{nodeslab::NodeSlab, WebConfig}; +use crate::WebConfig; pub struct WebsysDom { - document: Document, - pub interpreter: Interpreter, pub(crate) root: Element, - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, + handler: Closure, } -type ListenerEntry = (usize, Closure); - impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { - let document = load_document(); + // eventually, we just want to let the interpreter do all the work of decoding events into our event type + let callback: Box = Box::new(move |event: &web_sys::Event| { + if let Ok(synthetic_event) = decode_trigger(event) { + // Try to prevent default if the attribute is set + if let Some(target) = event.target() { + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + } - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); + sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + }); let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); Self { interpreter: Interpreter::new(root.clone()), - listeners, - document, - sender_callback, + handler: Closure::wrap(callback), root, } } @@ -78,7 +77,12 @@ impl WebsysDom { event_name, scope, root, - } => self.interpreter.NewEventListener(event_name, scope.0, root), + } => { + // + let handler: &Function = self.handler.as_ref().unchecked_ref(); + self.interpreter + .NewEventListener(event_name, scope.0, root, handler); + } DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } @@ -97,37 +101,6 @@ impl WebsysDom { } } -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} - pub struct DioxusWebsysEvent(web_sys::Event); // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread @@ -267,6 +240,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(()), "wheel" => { let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js deleted file mode 100644 index 1d0f14c68..000000000 --- a/packages/web/src/interpreter.js +++ /dev/null @@ -1,529 +0,0 @@ -"use strict"; -var exports = {}; -exports.__esModule = true; -exports.Interpreter = void 0; -function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - case "compositionend": - case "compositionstart": - case "compositionupdate": - var data = event.data; - return { - data: data, - }; - case "keydown": - case "keypress": - case "keyup": - var _c = event, - charCode = _c.charCode, - key = _c.key, - altKey = _c.altKey, - ctrlKey = _c.ctrlKey, - metaKey = _c.metaKey, - keyCode = _c.keyCode, - shiftKey = _c.shiftKey, - location_1 = _c.location, - repeat = _c.repeat, - which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - case "focus": - case "blur": - return {}; - case "change": - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = - (_a = target.value) !== null && _a !== void 0 - ? _a - : target.textContent; - } - return { - value: value, - }; - case "input": - case "invalid": - case "reset": - case "submit": { - var target_1 = event.target; - var value_1 = - (_b = target_1.value) !== null && _b !== void 0 - ? _b - : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; - } - return { - value: value_1, - }; - } - 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": { - var _d = event, - altKey_1 = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey_1 = _d.ctrlKey, - metaKey_1 = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey_1 = _d.shiftKey; - return { - alt_key: altKey_1, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, - page_x: pageX, - page_y: pageY, - screen_x: screenX_1, - screen_y: screenY_1, - shift_key: shiftKey_1, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey_2 = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey_2 = _e.ctrlKey, - metaKey_2 = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey_2 = _e.shiftKey, - pointerId = _e.pointerId, - width = _e.width, - height = _e.height, - pressure = _e.pressure, - tangentialPressure = _e.tangentialPressure, - tiltX = _e.tiltX, - tiltY = _e.tiltY, - twist = _e.twist, - pointerType = _e.pointerType, - isPrimary = _e.isPrimary; - return { - alt_key: altKey_2, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, - page_x: pageX, - page_y: pageY, - screen_x: screenX_2, - screen_y: screenY_2, - shift_key: shiftKey_2, - 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": { - var _f = event, - altKey_3 = _f.altKey, - ctrlKey_3 = _f.ctrlKey, - metaKey_3 = _f.metaKey, - shiftKey_3 = _f.shiftKey; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3, - }; - } - case "scroll": - return {}; - case "wheel": { - var _g = event, - deltaX = _g.deltaX, - deltaY = _g.deltaY, - deltaZ = _g.deltaZ, - deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, - animationName = _h.animationName, - elapsedTime = _h.elapsedTime, - pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, - propertyName = _j.propertyName, - elapsedTime = _j.elapsedTime, - pseudoElement = _j.pseudoElement; - 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 {}; - } -} -var 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, -}; -export var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root) { - console.log("new event listener", event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute( - "dioxus-event-".concat(event_name), - "".concat(scope, ".").concat(root) - ); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } - }; - Interpreter.prototype.RemoveEventListener = function ( - root, - event_name, - scope - ) { - // - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var 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); - } - } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - console.log("handling edits ", edits); - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (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, edit.scope); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, 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; - } - }; - return Interpreter; -})(); -exports.Interpreter = Interpreter; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ba9398276..8c0c0c3b4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -65,7 +65,6 @@ pub(crate) mod bindings; mod cache; mod cfg; mod dom; -mod nodeslab; mod rehydrate; mod ric_raf; diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs deleted file mode 100644 index b986bd34e..000000000 --- a/packages/web/src/nodeslab.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs index 364f1a1d7..8654ced90 100644 --- a/packages/web/src/olddom.rs +++ b/packages/web/src/olddom.rs @@ -787,3 +787,71 @@ fn event_name_from_typ(typ: &str) -> &'static str { } } } + + +//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. + +use std::ops::{Index, IndexMut}; +use web_sys::Node; + +pub(crate) struct NodeSlab { + nodes: Vec>, +} + +impl NodeSlab { + pub fn new(capacity: usize) -> NodeSlab { + let nodes = Vec::with_capacity(capacity); + NodeSlab { nodes } + } +} +impl Index for NodeSlab { + type Output = Option; + fn index(&self, index: usize) -> &Self::Output { + &self.nodes[index] + } +} + +impl IndexMut for NodeSlab { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + if index >= self.nodes.capacity() * 3 { + panic!("Trying to mutate an element way too far out of bounds"); + } + + if index + 1 > self.nodes.len() { + self.nodes.resize_with(index + 1, || None); + } + &mut self.nodes[index] + } +} + + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 25b126036..b62cba753 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -118,7 +118,7 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; } place.pop(); @@ -146,7 +146,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 29c97bcba..2ac0668b9 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -68,8 +68,7 @@ impl RafLoop { 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 = TimeoutFuture::new(deadline); - deadline + TimeoutFuture::new(deadline) } pub async fn wait_for_raf(&self) { From 81c094ed29fcdc5c6099492dd6ab09a59b79252c Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 14:55:12 -0600 Subject: [PATCH 013/244] Fix handling of re-renders in the Router Previously the router just stored a `root_found` boolean after it picked a route. But on re-render it would just always return false from `should_render` if this was true. This just aborted routing after a future resolved (or anything else that triggered a re-render). Now we store the matching ScopeId and check that against our routes in a re-render so we actually do re-render the matching route. --- packages/router/src/service.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 2fac8509b..00642414f 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -11,7 +11,7 @@ pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, slots: Rc>>, - root_found: Rc>, + root_found: Rc>>, cur_path_params: Rc>>, listener: HistoryListener, } @@ -39,11 +39,11 @@ impl RouterService { let _slots = slots.clone(); - let root_found = Rc::new(Cell::new(false)); + 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(false); + _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); @@ -74,8 +74,13 @@ impl RouterService { pub fn should_render(&self, scope: ScopeId) -> bool { log::trace!("Should render scope id {:?}?", scope); - if self.root_found.get() { - log::trace!(" no - because root_found is true"); + 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; } @@ -89,7 +94,7 @@ impl RouterService { // fallback logic match root { - Some((_id, route)) => { + Some((id, route)) => { log::trace!( " matched given scope id {:?} with route root '{}'", scope, @@ -97,13 +102,13 @@ impl RouterService { ); if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); - self.root_found.set(true); + 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(true); + 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); From 9b282d877ba0fe2463687cde7ea899b7f8510425 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:40:01 -0600 Subject: [PATCH 014/244] Don't expect all components to have a scope in ScopeArena.ensure_drop_safety Not all components will be mounted when using a Router, so we cannot assume all components have a scope. --- packages/core/src/scopes.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 65511470e..5b8f527c5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -198,12 +198,9 @@ impl ScopeArena { // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children items.borrowed_props.drain(..).for_each(|comp| { - let scope_id = comp - .scope - .get() - .expect("VComponents should be associated with a valid Scope"); - - self.ensure_drop_safety(scope_id); + if let Some(scope_id) = comp.scope.get() { + self.ensure_drop_safety(scope_id); + } drop(comp.props.take()); }); From baf722de11efca34706e11fc80bcdc28efcabf5d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 Jan 2022 20:14:51 -0500 Subject: [PATCH 015/244] 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 e47ead5347ce778935f8f2127fcb14f520eda0ce Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 Jan 2022 20:17:48 -0500 Subject: [PATCH 016/244] fix: allow eventhandler to derive default --- packages/core/src/nodes.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d662b4998..93d4c8b10 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -351,8 +351,9 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` +#[derive(Default)] pub struct EventHandler<'bump, T = ()> { - pub callback: &'bump RefCell>>, + pub callback: RefCell>>, } impl EventHandler<'_, T> { @@ -367,15 +368,6 @@ impl EventHandler<'_, T> { } } -impl Copy for EventHandler<'_, T> {} -impl Clone for EventHandler<'_, T> { - fn clone(&self) -> Self { - Self { - callback: self.callback, - } - } -} - /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { @@ -677,7 +669,7 @@ impl<'a> NodeFactory<'a> { pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { let handler: &mut dyn FnMut(T) = self.bump.alloc(f); let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = self.bump.alloc(RefCell::new(Some(caller))); + let callback = RefCell::new(Some(caller)); EventHandler { callback } } } From 036a0ff49a7dade0e04c9c07071a1ff49133ee24 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:13:31 -0500 Subject: [PATCH 017/244] docs: add comments for the Handler --- packages/core/src/nodes.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 93d4c8b10..80de92917 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -357,12 +357,14 @@ pub struct EventHandler<'bump, T = ()> { } impl EventHandler<'_, T> { + /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); } } + /// Forcibly drop the internal handler callback, releasing memory pub fn release(&self) { self.callback.replace(None); } From 58839f47bae3c49cadd4b41da9a5debebb1def99 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:56:48 -0500 Subject: [PATCH 018/244] 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 5badcd830cdc1d19faab8fb516baaebc09168c62 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:44:16 +0800 Subject: [PATCH 019/244] Update ZH_CN.md --- notes/README/ZH_CN.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index dfc5b3e89..680409e88 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -82,7 +82,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 如果你会使用 React ,那 Dioxus 对你来说会很简单。 ### 项目特点: -- 桌面程序在本地运行。 +- 对桌面应用的原生支持。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 @@ -126,30 +126,30 @@ TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScri TS 代码运行效率不高,而且有大量的配置项。 -相比之下,Dioxus 使用用 Rust 编写将大大的提高效能。 +相比之下,Dioxus 使用 Rust 编写将大大的提高效能。 使用 Rust 开发,我们能获得: -- 库的静态类型支持。 -- 变量的默认不变性。 -- 一个简单且直观的模块管理系统。 -- 集成在内部的文档系统。 +- 静态类型支持。 +- 变量默认不变性。 +- 简单直观的模块系统。 +- 内部集成的文档系统。 - 先进的模式匹配系统。 -- 简洁、高效、可组合的迭代器。 -- 内联内置的 单元测试 / 集成测试。 -- 一流的异常处理系统。 +- 简洁、高效、强大的迭代器。 +- 内置的 单元测试 / 集成测试。 +- 优秀的异常处理系统。 - 强大且健全的标准库。 - 灵活的 `宏` 系统。 - 使用 `crates.io` 管理包。 Dioxus 能为开发者提供的: -- 正确且安全的使用不可变数据结构。 +- 安全使用数据结构。 - 安全的错误处理结果。 - 拥有原生移动端的性能。 - 直接访问系统的IO层。 -Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。 ### 不建议使用 Dioxus 的情况? From 27bdef304083e8826b2dddbef5c0a0136f3898b1 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:47:38 +0800 Subject: [PATCH 020/244] Update ZH_CN.md --- notes/README/ZH_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 680409e88..614774595 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -86,7 +86,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 -- 多通道异步调动器,超强的异步支持。 +- 多通道异步调度器,超强的异步支持。 - 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). ### 示例 @@ -155,7 +155,7 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 您不该在这些情况下使用 Dioxus : -- 您不喜欢 React Hooks 的前端开发方案。 +- 您不喜欢类似 React 的开发风格。 - 您需要一个 `no-std` 的渲染器。 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 - 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 From 64080588d02c54a1d380116cbecdd17de16d2392 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:44:18 -0600 Subject: [PATCH 021/244] Add a warning when Link it called outside of a Router context --- packages/router/src/components/link.rs | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index dc807aa1a..6a1b671ab 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -41,18 +41,25 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - let service = cx.consume_context::()?; - 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("")), + log::debug!("render Link to {}", cx.props.to); + if let Some(service) = cx.consume_context::() { + 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), + prevent_default: "onclick", + onclick: move |_| service.push_route(cx.props.to), - &cx.props.children - } - }) + &cx.props.children + } + }); + } + log::warn!( + "Attempted to create a Link to {} outside of a Router context", + cx.props.to, + ); + None } From a0f60152bc7e5866f114ed469809ce8be70d17d4 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:12 +0800 Subject: [PATCH 022/244] feat: link open in browser --- examples/link.rs | 27 +++++++++++++++++++++++++++ packages/desktop/Cargo.toml | 1 + packages/desktop/src/index.js | 25 ++++++++++++++++++++++++- packages/desktop/src/lib.rs | 21 +++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 examples/link.rs diff --git a/examples/link.rs b/examples/link.rs new file mode 100644 index 000000000..bed14f8a2 --- /dev/null +++ b/examples/link.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! ( + div { + p { + a { + href: "http://dioxuslabs.com/", + "default link" + } + } + p { + a { + href: "http://dioxuslabs.com/", + onclick: |_| { + println!("Hello Dioxus"); + }, + "custom event link", + } + } + } + )) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8a0e5e8f5..eb9425c1f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1.12.0", features = [ ], 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"} +webbrowser = "0.5.5" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 88cf9fcdc..b7de5bf0d 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,7 @@ class Interpreter { } SetAttribute(edit) { - // console.log("setting attr", edit); + console.log("setting attr", edit); const name = edit.field; const value = edit.value; const ns = edit.ns; @@ -399,6 +399,29 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; + break; + case "href": + + if (node.tagName == "A") { + // open the tag in browser + node.setAttribute("browser-href", value); + node.setAttribute("href", "#"); + + node.addEventListener("click", function(event) { + + const target = event.target; + const real_id = target.getAttribute(`dioxus-id`); + + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }) + }) + + } else { + node.setAttribute(name, value); + } + break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..546db27c1 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -187,6 +187,27 @@ pub fn launch_with_props( is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let _ = proxy.send_event(UserWindowEvent::Update); } + "browser_open" => { + let data = req.params.unwrap(); + log::trace!("Open browser: {:?}", data); + if data.is_array() { + let arr = data.as_array().unwrap(); + if arr[0].is_object() { + let temp = arr[0] + .as_object() + .unwrap(); + if temp.contains_key("href") { + let url = temp + .get("href").unwrap() + .as_str().unwrap(); + match webbrowser::open(url) { + Ok(_) => {}, + Err(e) => log::trace!("Open browser error: {:?}", e), + } + } + } + } + } _ => {} } None From c737c424b05ad8453e8770a14a0d210fb0c7c2fe Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:58 +0800 Subject: [PATCH 023/244] feat: link open in browser --- packages/desktop/src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index b7de5bf0d..d15fc39b0 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,9 @@ class Interpreter { } SetAttribute(edit) { - console.log("setting attr", edit); + + // console.log("setting attr", edit); + const name = edit.field; const value = edit.value; const ns = edit.ns; From 5233ee97d9314f7f0e0bdf05c56d2a9e4201a596 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:20:36 +0800 Subject: [PATCH 024/244] fix: format code --- examples/link.rs | 2 +- packages/desktop/src/lib.rs | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index bed14f8a2..da4b5b459 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -6,7 +6,7 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! ( - div { + div { p { a { href: "http://dioxuslabs.com/", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 546db27c1..0f451bd0d 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -193,16 +193,14 @@ pub fn launch_with_props( if data.is_array() { let arr = data.as_array().unwrap(); if arr[0].is_object() { - let temp = arr[0] - .as_object() - .unwrap(); + let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { - let url = temp - .get("href").unwrap() - .as_str().unwrap(); + let url = temp.get("href").unwrap().as_str().unwrap(); match webbrowser::open(url) { - Ok(_) => {}, - Err(e) => log::trace!("Open browser error: {:?}", e), + Ok(_) => {} + Err(e) => { + log::trace!("Open browser error: {:?}", e) + } } } } From 9e04ce5342850d2e0a01dde169807d6f6eb16566 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:32:27 +0800 Subject: [PATCH 025/244] feat: `open_browser` bool attribute --- examples/link.rs | 1 + packages/desktop/src/index.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index da4b5b459..49921f98a 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", + "browser-open": "false", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index d15fc39b0..c141f56d4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -414,10 +414,15 @@ class Interpreter { const target = event.target; const real_id = target.getAttribute(`dioxus-id`); - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }) + if (target.getAttribute("browser-open") != "false") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }); + } else { + window.location = target.getAttribute("browser-href"); + } + }) } else { From f006f50317f4b75fac353bc988db057a281ba7f8 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:52:14 +0800 Subject: [PATCH 026/244] feat: move `rpc` to handler --- examples/link.rs | 2 +- packages/desktop/src/index.js | 43 ++++++++++++----------------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index 49921f98a..41b010e8d 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", - "browser-open": "false", + prevent_default: "onclick", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index c141f56d4..0bd767581 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -343,6 +343,7 @@ class Interpreter { this.listeners[event_name] = true; this.root.addEventListener(event_name, (event) => { + const target = event.target; const real_id = target.getAttribute(`dioxus-id`); @@ -356,6 +357,20 @@ class Interpreter { event.preventDefault(); } + console.log(event); + if (event.type == "click") { + event.preventDefault(); + if (should_prevent_default !== `onclick`) { + console.log(event.target.getAttribute("href")); + if(element.tagName == "A") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }) + } + } + } + if (real_id == null) { return; } @@ -401,34 +416,6 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; - break; - case "href": - - if (node.tagName == "A") { - // open the tag in browser - node.setAttribute("browser-href", value); - node.setAttribute("href", "#"); - - node.addEventListener("click", function(event) { - - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - if (target.getAttribute("browser-open") != "false") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }); - } else { - window.location = target.getAttribute("browser-href"); - } - - }) - - } else { - node.setAttribute(name, value); - } - break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 From 62b637f8b0eaf616c49461fa23b9251a79abc147 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:54:48 +0800 Subject: [PATCH 027/244] fix: error pattern --- packages/desktop/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0f451bd0d..38c85052a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -196,11 +196,8 @@ pub fn launch_with_props( let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); - match webbrowser::open(url) { - Ok(_) => {} - Err(e) => { - log::trace!("Open browser error: {:?}", e) - } + if let Err(e) = webbrowser::open(url) { + log::error!("Open Browser error: {:?}", e); } } } From 46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:57:41 +0800 Subject: [PATCH 028/244] feat: link open in browser --- packages/desktop/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 38c85052a..8526a4a93 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -190,10 +190,8 @@ pub fn launch_with_props( "browser_open" => { let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); - if data.is_array() { - let arr = data.as_array().unwrap(); - if arr[0].is_object() { - let temp = arr[0].as_object().unwrap(); + if let Some(arr) = data.as_array() { + if let Some(temp) = arr[0].as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { From 8089023a6c3a54957af9c9c05c9dee6088b059ef Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 15:00:49 +0800 Subject: [PATCH 029/244] fix: prevent `submit` default --- packages/desktop/src/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 0bd767581..3ab21d6ba 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -357,7 +357,10 @@ class Interpreter { event.preventDefault(); } - console.log(event); + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { From 327f9015481809d8e5b9e69f26202e8d66dd198e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:15:39 +0800 Subject: [PATCH 030/244] fix: check `href` null --- packages/desktop/src/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 3ab21d6ba..685d1c7be 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -364,12 +364,14 @@ class Interpreter { if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { - console.log(event.target.getAttribute("href")); if(element.tagName == "A") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: event.target.getAttribute("href") - }) + const href = event.target.getAttribute("href") + if (href !== "" || href !== null) { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }); + } } } } From 2073b400df55f0c6d8bed7371b2313be6c064e6e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:25:45 +0800 Subject: [PATCH 031/244] fix: check `href` null --- packages/desktop/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 685d1c7be..2aba572f4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -366,7 +366,7 @@ class Interpreter { if (should_prevent_default !== `onclick`) { if(element.tagName == "A") { const href = event.target.getAttribute("href") - if (href !== "" || href !== null) { + if (href !== "" && href !== null && href !== undefined) { rpc.call("browser_open", { mounted_dom_id: parseInt(real_id), href: event.target.getAttribute("href") From 16d2074fcf9187e14b7676aa9672e8ede92224fd Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Wed, 19 Jan 2022 20:14:41 +0800 Subject: [PATCH 032/244] fix: `dog-app` breed change --- examples/dog_app.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 3b35ae174..b2f8429ee 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -61,7 +61,7 @@ fn app(cx: Scope) -> Element { #[inline_props] fn Breed(cx: Scope, breed: String) -> Element { - #[derive(serde::Deserialize)] + #[derive(serde::Deserialize, Debug)] struct DogApi { message: String, } @@ -72,6 +72,12 @@ 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()); + fut.restart(); + } + cx.render(match fut.value() { Some(Ok(resp)) => rsx! { button { From c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 20 Jan 2022 04:01:38 -0500 Subject: [PATCH 033/244] fix: ssr + hydration event listeners --- packages/web/src/dom.rs | 20 ++++++++------------ packages/web/src/rehydrate.rs | 11 +++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 364f1a1d7..219f5aa79 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -75,10 +75,8 @@ impl WebsysDom { } DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), DomEdit::NewEventListener { - event_name, - scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), + event_name, root, .. + } => self.new_event_listener(event_name, root), DomEdit::RemoveEventListener { event, root } => { self.remove_event_listener(event, root) @@ -172,10 +170,8 @@ impl WebsysDom { let node = self.nodes[root as usize].as_ref().unwrap(); if let Some(element) = node.dyn_ref::() { element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } + } else if let Some(parent) = node.parent_node() { + parent.remove_child(node).unwrap(); } } @@ -227,7 +223,7 @@ impl WebsysDom { self.nodes[(id as usize)] = Some(el); } - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + pub fn new_event_listener(&mut self, event: &'static str, node: u64) { let event = wasm_bindgen::intern(event); // attach the correct attributes to the element @@ -235,7 +231,7 @@ impl WebsysDom { // This ensures we only ever have one handler attached to the root, but decide // dynamically when we want to call a listener. - let el = self.stack.top(); + let el = self.nodes[(node as usize)].as_ref().unwrap(); let el = el.dyn_ref::().unwrap(); @@ -278,7 +274,7 @@ impl WebsysDom { .unwrap(); // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); + self.listeners.insert(event, (1, handler)); } } @@ -442,7 +438,7 @@ impl WebsysDom { anchor .parent_node() .unwrap() - .insert_before(&before, Some(&anchor)) + .insert_before(&before, Some(anchor)) .unwrap(); } else { let arr: js_sys::Array = self diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..91abb4fa1 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -116,7 +116,14 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; + } + + for listener in vel.listeners { + self.new_event_listener( + listener.event, + listener.mounted_node.get().unwrap().as_u64(), + ); } place.pop(); @@ -142,7 +149,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } } From 50f90d889affbd355f2f9caa4e5c039ab44500db Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 00:21:23 -0500 Subject: [PATCH 034/244] fix: bump to readd desktop click handling code --- packages/jsinterpreter/interpreter.ts | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 17617a209..533536277 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -558,31 +558,50 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - // todo: only on desktop should we make our own handler + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation let handler = (event: Event) => { const target = event.target as Element | null; - // console.log("event", event); + if (target != null) { + const realId = target.getAttribute(`dioxus-id`); - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); - if (should_prevent_default === `on${event.type}`) { + if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (real_id == null) { + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { return; } window.rpc.call("user_event", { event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(real_id), + mounted_dom_id: parseInt(realId), contents: contents, }); } @@ -655,4 +674,3 @@ declare global { } -type Edits = DomEdit[]; From d2bd1751436ef4bec554bb361ba87aea1357036a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 00:37:52 -0500 Subject: [PATCH 035/244] feat: allow providing context to the root component --- packages/core/src/scopes.rs | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5b8f527c5..a4b32e50d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -667,6 +667,56 @@ impl ScopeState { value } + /// Provide a context for the root component from anywhere in your app. + /// + /// + /// # Example + /// + /// ```rust, ignore + /// struct SharedState(&'static str); + /// + /// static App: Component = |cx| { + /// cx.use_hook(|_| cx.provide_root_context(SharedState("world"))); + /// rsx!(cx, Child {}) + /// } + /// + /// static Child: Component = |cx| { + /// let state = cx.consume_state::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn provide_root_context(&self, value: T) -> Rc { + let value = Rc::new(value); + + // if we *are* the root component, then we can just provide the context directly + if self.scope_id() == ScopeId(0) { + self.shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()) + .map(|f| f.downcast::().ok()) + .flatten(); + } + + let mut search_parent = self.parent_scope; + + while let Some(parent) = search_parent.take() { + let parent = unsafe { &*parent }; + + if parent + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()) + .is_some() + { + log::warn!("Context already provided to parent scope - replacing it"); + } + + search_parent = parent.parent_scope; + } + + value + } + /// Try to retrieve a SharedState with type T from the any parent Scope. pub fn consume_context(&self) -> Option> { if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { From c8d528b3b18e320ca0cee1542ae5fc659c1cca81 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 09:29:41 -0500 Subject: [PATCH 036/244] fix: proprogation of root context --- packages/core/src/scopes.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index a4b32e50d..cfc555543 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -702,19 +702,22 @@ impl ScopeState { while let Some(parent) = search_parent.take() { let parent = unsafe { &*parent }; - if parent - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), value.clone()) - .is_some() - { - log::warn!("Context already provided to parent scope - replacing it"); + if parent.scope_id() == ScopeId(0) { + let exists = parent + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()); + + if exists.is_some() { + log::warn!("Context already provided to parent scope - replacing it"); + } + return value; } search_parent = parent.parent_scope; } - value + unreachable!("all apps have a root scope") } /// Try to retrieve a SharedState with type T from the any parent Scope. From 997684a39094ccd51bbab96210f400df8af036fa Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 09:53:25 -0500 Subject: [PATCH 037/244] fix: update desktop intrepreter with browser open --- packages/jsinterpreter/interpreter.js | 1024 ++++++++++++++----------- 1 file changed, 558 insertions(+), 466 deletions(-) diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 453b34406..9da95fa7c 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,477 +1,569 @@ function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - var data = event.data; - return { - data: data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - var target = event.target; - var value = (_b = target.value) !== null && _b !== void 0 ? _b : 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": { - var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey; - 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_1, - screen_y: screenY_1, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; - 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_2, - screen_y: screenY_2, - 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": { - var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey; - 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": { - var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement; - 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 {}; - } + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + var data = event.data; + return { + data: data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + var _c = event, + charCode = _c.charCode, + key = _c.key, + altKey = _c.altKey, + ctrlKey = _c.ctrlKey, + metaKey = _c.metaKey, + keyCode = _c.keyCode, + shiftKey = _c.shiftKey, + location_1 = _c.location, + repeat = _c.repeat, + which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = + (_a = target.value) !== null && _a !== void 0 + ? _a + : target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + var target = event.target; + var value = + (_b = target.value) !== null && _b !== void 0 ? _b : 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": { + var _d = event, + altKey = _d.altKey, + button = _d.button, + buttons = _d.buttons, + clientX = _d.clientX, + clientY = _d.clientY, + ctrlKey = _d.ctrlKey, + metaKey = _d.metaKey, + pageX = _d.pageX, + pageY = _d.pageY, + screenX_1 = _d.screenX, + screenY_1 = _d.screenY, + shiftKey = _d.shiftKey; + 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_1, + screen_y: screenY_1, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, + altKey = _e.altKey, + button = _e.button, + buttons = _e.buttons, + clientX = _e.clientX, + clientY = _e.clientY, + ctrlKey = _e.ctrlKey, + metaKey = _e.metaKey, + pageX = _e.pageX, + pageY = _e.pageY, + screenX_2 = _e.screenX, + screenY_2 = _e.screenY, + shiftKey = _e.shiftKey, + pointerId = _e.pointerId, + width = _e.width, + height = _e.height, + pressure = _e.pressure, + tangentialPressure = _e.tangentialPressure, + tiltX = _e.tiltX, + tiltY = _e.tiltY, + twist = _e.twist, + pointerType = _e.pointerType, + isPrimary = _e.isPrimary; + 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_2, + screen_y: screenY_2, + 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": { + var _f = event, + altKey = _f.altKey, + ctrlKey = _f.ctrlKey, + metaKey = _f.metaKey, + shiftKey = _f.shiftKey; + 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": { + var _g = event, + deltaX = _g.deltaX, + deltaY = _g.deltaY, + deltaZ = _g.deltaZ, + deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, + animationName = _h.animationName, + elapsedTime = _h.elapsedTime, + pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + var _j = event, + propertyName = _j.propertyName, + elapsedTime = _j.elapsedTime, + pseudoElement = _j.pseudoElement; + 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 {}; + } + } } var 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, }; var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { + root.appendChild(to_add[i]); } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) { - // console.log('new event listener', event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(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]++; - } - }; - Interpreter.prototype.RemoveEventListener = function (root, event_name) { - var element = this.nodes[root]; - element.removeAttribute("dioxus-event-".concat(event_name)); - 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]; - } - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var 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); - } + }; + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (text, root) { + // todo: make it so the types are okay + var node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (root) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.NewEventListener = function ( + event_name, + scope, + root, + handler + ) { + // console.log('new event listener', event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute( + "dioxus-event-".concat(event_name), + "".concat(scope, ".").concat(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]++; + } + }; + Interpreter.prototype.RemoveEventListener = function (root, event_name) { + var element = this.nodes[root]; + element.removeAttribute("dioxus-event-".concat(event_name)); + 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]; + } + }; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var 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); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + this.stack.push(this.root); + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (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": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + // console.log("event", event); + if (target != null) { + var realId = target.getAttribute("dioxus-id"); + var shouldPreventDefault = target.getAttribute( + "dioxus-prevent-default" + ); + var contents = serialize_event(event); + if (shouldPreventDefault === "on".concat(event.type)) { + event.preventDefault(); } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (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": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - // console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute("dioxus-prevent-default"); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, 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; - } - }; - return Interpreter; -}()); + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + var href = target.getAttribute("href"); + if ( + href !== "" && + href !== null && + href !== undefined && + realId != null + ) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href: 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.scope, 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; + } + }; + return Interpreter; +})(); export { Interpreter }; function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } } -//# sourceMappingURL=interpreter.js.map \ No newline at end of file +//# sourceMappingURL=interpreter.js.map From 95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 15:06:57 -0500 Subject: [PATCH 038/244] feat: remove dioxus id on non-event elements --- examples/nested_listeners.rs | 33 + packages/desktop/src/index.html | 8 +- packages/desktop/src/lib.rs | 10 +- packages/jsinterpreter/interpreter.js | 1060 ++++++++++++------------- packages/jsinterpreter/interpreter.ts | 628 +++++++-------- packages/jsinterpreter/tsconfig.json | 6 +- packages/web/src/dom.rs | 46 +- 7 files changed, 888 insertions(+), 903 deletions(-) create mode 100644 examples/nested_listeners.rs diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs new file mode 100644 index 000000000..4b9fd14e0 --- /dev/null +++ b/examples/nested_listeners.rs @@ -0,0 +1,33 @@ +//! Nested Listeners +//! +//! This example showcases how to control event bubbling from child to parents. +//! +//! Both web and desktop support bubbling and bubble cancelation. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| println!("clicked! top"), + button { + onclick: move |_| println!("clicked! bottom propoate"), + "Propogate" + } + button { + onclick: move |evt| { + println!("clicked! bottom no bubbling"); + evt.cancel_bubble(); + }, + "Dont propogate" + } + button { + "Does not handle clicks" + } + } + }) +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index c17a63d2a..88fcba12a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -13,8 +13,10 @@ - - - + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 802e7441c..bc9a84e42 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -205,7 +205,7 @@ pub fn launch_with_props( } None }) - .with_custom_protocol("dioxus".into(), move |request| { + .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://", ""); @@ -225,10 +225,10 @@ pub fn launch_with_props( } }) .with_file_drop_handler(move |window, evet| { - if let Some(handler) = file_handler.as_ref() { - return handler(window, evet); - } - false + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() }); for (name, handler) in cfg.protocos.drain(..) { diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 9da95fa7c..ea50ec3d3 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,569 +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) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": { - 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 {}; + } } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - var data = event.data; - return { - data: data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - var _c = event, - charCode = _c.charCode, - key = _c.key, - altKey = _c.altKey, - ctrlKey = _c.ctrlKey, - metaKey = _c.metaKey, - keyCode = _c.keyCode, - shiftKey = _c.shiftKey, - location_1 = _c.location, - repeat = _c.repeat, - which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = - (_a = target.value) !== null && _a !== void 0 - ? _a - : target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - var target = event.target; - var value = - (_b = target.value) !== null && _b !== void 0 ? _b : 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": { - var _d = event, - altKey = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey = _d.ctrlKey, - metaKey = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey = _d.shiftKey; - 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_1, - screen_y: screenY_1, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey = _e.ctrlKey, - metaKey = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey = _e.shiftKey, - pointerId = _e.pointerId, - width = _e.width, - height = _e.height, - pressure = _e.pressure, - tangentialPressure = _e.tangentialPressure, - tiltX = _e.tiltX, - tiltY = _e.tiltY, - twist = _e.twist, - pointerType = _e.pointerType, - isPrimary = _e.isPrimary; - 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_2, - screen_y: screenY_2, - 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": { - var _f = event, - altKey = _f.altKey, - ctrlKey = _f.ctrlKey, - metaKey = _f.metaKey, - shiftKey = _f.shiftKey; - 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": { - var _g = event, - deltaX = _g.deltaX, - deltaY = _g.deltaY, - deltaZ = _g.deltaZ, - deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, - animationName = _h.animationName, - elapsedTime = _h.elapsedTime, - pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, - propertyName = _j.propertyName, - elapsedTime = _j.elapsedTime, - pseudoElement = _j.pseudoElement; - 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 {}; - } - } } -var 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, +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, }; -var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function ( - event_name, - scope, - root, - handler - ) { - // console.log('new event listener', event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute( - "dioxus-event-".concat(event_name), - "".concat(scope, ".").concat(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]++; - } - }; - Interpreter.prototype.RemoveEventListener = function (root, event_name) { - var element = this.nodes[root]; - element.removeAttribute("dioxus-event-".concat(event_name)); - 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]; - } - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var 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); - } - } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (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": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - // console.log("event", event); - if (target != null) { - var realId = target.getAttribute("dioxus-id"); - var shouldPreventDefault = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (shouldPreventDefault === "on".concat(event.type)) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - var href = target.getAttribute("href"); - if ( - href !== "" && - href !== null && - href !== undefined && - realId != null - ) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href: 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.scope, 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; - } - }; - return Interpreter; -})(); -export { Interpreter }; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -//# sourceMappingURL=interpreter.js.map diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 533536277..fdd8c671d 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,3 +1,319 @@ + + + + +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +export class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(root: number) { + const node = this.nodes[root]; + this.stack.push(node); + } + + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; + + let to_add = this.stack.splice(this.stack.length - many); + + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(root_id: number, m: number) { + let root = this.nodes[root_id] as Element; + let els = this.stack.splice(this.stack.length - m); + + root.replaceWith(...els); + } + + InsertAfter(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + + InsertBefore(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + + Remove(root: number) { + let node = this.nodes[root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(text: string, root: number) { + // todo: make it so the types are okay + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; + this.stack.push(node); + } + + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + + this.nodes[root] = el; + this.stack.push(el); + } + + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + + CreatePlaceholder(root: number) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + + NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + + RemoveEventListener(root: number, event_name: string) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + + this.listeners[event_name]--; + + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + + + SetText(root: number, text: string) { + this.nodes[root].textContent = text; + } + + SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + const name = field; + const node = this.nodes[root]; + + if (ns == "style") { + + // @ts-ignore + (node as HTMLElement).style[name] = value; + + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != (node as HTMLInputElement).value) { + (node as HTMLInputElement).value = value; + } + break; + case "checked": + (node as HTMLInputElement).checked = value === "true"; + break; + case "selected": + (node as HTMLOptionElement).selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root: number, name: string) { + + const node = this.nodes[root]; + node.removeAttribute(name); + + if (name === "value") { + (node as HTMLInputElement).value = ""; + } + + if (name === "checked") { + (node as HTMLInputElement).checked = false; + } + + if (name === "selected") { + (node as HTMLOptionElement).selected = false; + } + } + + handleEdits(edits: DomEdit[]) { + this.stack.push(this.root); + + for (let edit of edits) { + this.handleEdit(edit); + } + } + + handleEdit(edit: DomEdit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event: Event) => { + let target = event.target as Element | null; + + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + + let contents = serialize_event(event); + + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} + + + function serialize_event(event: Event) { switch (event.type) { case "copy": @@ -327,307 +643,6 @@ const bool_attrs = { truespeed: true, }; -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("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, scope: number, root: number, handler: (evt: Event) => void) { - // console.log('new event listener', event_name, root, scope); - const element = this.nodes[root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${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( - `dioxus-event-${event_name}` - ); - - 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) => { - const target = event.target as Element | null; - - if (target != null) { - const realId = target.getAttribute(`dioxus-id`); - - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } - - if (realId == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, 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 main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} type PushRoot = { type: "PushRoot", root: number }; @@ -663,14 +678,3 @@ type DomEdit = SetText | SetAttribute | RemoveAttribute; - - -export { }; -declare global { - interface Window { - interpreter: Interpreter; - rpc: { call: (method: string, args?: any) => void }; - } -} - - diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 883760881..f63034044 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "ES5", - "module": "ESNext", + "target": "ESNext", + "module": "ES2015", "lib": [ "es2015", "es5", "es6", "dom" ], - "strict": true + "strict": true, } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b0b4dd035..ffbef9f4b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -277,9 +277,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { - use anyhow::Context; - - let target = event + let mut target = event .target() .expect("missing target") .dyn_into::() @@ -287,18 +285,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let typ = event.type_(); - let element_id = target - .get_attribute("dioxus-id") - .context("Could not find element id on event target")? - .parse()?; - - Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(element_id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }) + 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 { From 9e4ec43b1e78d355c56a38e4c092170b2b01b20d Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 21 Jan 2022 21:43:43 -0600 Subject: [PATCH 039/244] Fix various typos and grammar nits I also removed all trailing whitespace from lines since I have Emacs configured to highlight this. --- README.md | 18 +++++------ docs/guide/src/README.md | 12 ++++---- docs/guide/src/SUMMARY.md | 6 ++-- docs/guide/src/advanced-guides/00-index.md | 2 +- .../src/advanced-guides/10-concurrent-mode.md | 4 +-- .../src/advanced-guides/11-arena-memo.md | 8 ++--- docs/guide/src/advanced-guides/12-signals.md | 8 ++--- docs/guide/src/advanced-guides/13-subtrees.md | 4 +-- .../src/advanced-guides/custom-renderer.md | 2 +- docs/guide/src/async/index.md | 4 +-- docs/guide/src/elements/component_children.md | 8 ++--- docs/guide/src/elements/components.md | 26 ++++++++-------- .../src/elements/conditional_rendering.md | 4 +-- .../src/elements/exporting_components.md | 30 +++++++++---------- docs/guide/src/elements/index.md | 2 +- docs/guide/src/elements/lists.md | 12 ++++---- docs/guide/src/elements/vnodes.md | 8 ++--- docs/guide/src/final.md | 4 +-- docs/guide/src/hello_world.md | 8 ++--- docs/guide/src/interactivity/hooks.md | 10 +++---- docs/guide/src/interactivity/index.md | 14 ++++----- docs/guide/src/setup.md | 13 ++++---- docs/guide/src/state/index.md | 4 +-- packages/core-macro/src/lib.rs | 8 ++--- packages/core/README.md | 2 +- packages/core/architecture.md | 16 +++++----- packages/core/src/events.rs | 2 +- packages/core/src/nodes.rs | 4 +-- packages/core/src/scopes.rs | 4 +-- packages/core/src/util.rs | 2 +- packages/core/src/virtual_dom.rs | 8 ++--- packages/hooks/src/usestate/handle.rs | 2 +- packages/html/README.md | 18 +++++------ packages/liveview/README.md | 4 +-- packages/mobile/README.md | 2 +- packages/router/README.md | 2 +- packages/router/src/components/link.rs | 2 +- packages/ssr/README.md | 4 +-- src/lib.rs | 30 +++++++++---------- 39 files changed, 160 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 641f5a8b8..7b277d16a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ If you know React, then you already know Dioxus. - Comprehensive inline documentation - hover and guides for all HTML elements, listeners, and events. - Extremely memory efficient - 0 global allocations for steady-state components. - Multi-channel asynchronous scheduler for first-class async support. -- And more! Read the [full release post here](https://dioxuslabs.com/blog/introducing-dioxus/). +- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/). ### Examples @@ -121,9 +121,9 @@ See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for ## Why Dioxus and why Rust? -TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. +TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. -In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". +In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". By using Rust, we gain: @@ -141,12 +141,12 @@ By using Rust, we gain: Specifically, Dioxus provides us many other assurances: -- Proper use of immutable datastructures -- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) +- Proper use of immutable data structures +- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) - Native performance on mobile - Direct access to system IO -And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. +And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. ### Why NOT Dioxus? You shouldn't use Dioxus if: @@ -154,13 +154,13 @@ You shouldn't use Dioxus if: - You don't like the React Hooks approach to frontend - You need a no-std renderer - You want to support browsers where Wasm or asm.js are not supported. -- You need a Send+Sync UI solution (Dioxus is not currently ThreadSafe) +- You need a Send+Sync UI solution (Dioxus is not currently thread-safe) ### Comparison with other Rust UI frameworks -Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. +Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. - [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). -- [Percy](https://github.com/chinedufn/percy): Supports SSR but less emphasis on state management and event handling. +- [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c91f9f1bb..c00dcd274 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -22,7 +22,7 @@ In general, Dioxus and React share many functional similarities. If this guide i ## Multiplatform -Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `Html` feature enabled which can be disabled depending on your target renderer. +Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. Right now, we have several 1st-party renderers: - WebSys (for WASM) @@ -33,8 +33,7 @@ Right now, we have several 1st-party renderers: ### Web Support --- - -The Web is the most-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the Wasm limitation, not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). +The Web is the best-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the limitations of Wasm not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. @@ -45,9 +44,10 @@ Examples: - [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site) [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) + ### SSR Support --- -Dioxus supports server-side rendering! +Dioxus supports server-side rendering! For rendering statically to an `.html` file or from a WebServer, then you'll want to make sure the `ssr` feature is enabled in the `dioxus` crate and use the `dioxus::ssr` API. We don't expect the SSR API to change drastically in the future. @@ -90,13 +90,13 @@ Examples: ### LiveView / Server Component Support --- -The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. +The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA. ### Multithreaded Support --- -The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. +The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels. diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 2e77d4ab8..2fa29f39e 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,7 +1,7 @@ # Summary - [Introduction](README.md) -- [Getting Setup](setup.md) +- [Getting Set Up](setup.md) - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) - [Intro to Elements](elements/vnodes.md) @@ -17,8 +17,8 @@ - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) - - [Local State](state/localstate.md) - - [Lifting State](state/liftingstate.md) + - [Local State](state/localstate.md) + - [Lifting State](state/liftingstate.md) - [Global State](state/sharedstate.md) - [Error handling](state/errorhandling.md) - [Working with Async](async/index.md) diff --git a/docs/guide/src/advanced-guides/00-index.md b/docs/guide/src/advanced-guides/00-index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/advanced-guides/00-index.md +++ b/docs/guide/src/advanced-guides/00-index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/advanced-guides/10-concurrent-mode.md b/docs/guide/src/advanced-guides/10-concurrent-mode.md index 5d7801cb6..6bbebbeed 100644 --- a/docs/guide/src/advanced-guides/10-concurrent-mode.md +++ b/docs/guide/src/advanced-guides/10-concurrent-mode.md @@ -33,9 +33,9 @@ async fetch_name() -> String { This component will only schedule its render once the fetch is complete. However, we _don't_ recommend using async/await directly in your components. -Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion. +Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, causing confusion. -Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks. +Instead, we suggest using hooks and future combinators that can safely utilize the safeguards of the component's Context when interacting with async tasks. As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading. diff --git a/docs/guide/src/advanced-guides/11-arena-memo.md b/docs/guide/src/advanced-guides/11-arena-memo.md index d95a67d19..d5eaaa1d0 100644 --- a/docs/guide/src/advanced-guides/11-arena-memo.md +++ b/docs/guide/src/advanced-guides/11-arena-memo.md @@ -2,13 +2,13 @@ Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator. -One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components. +One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to `Any` and allocated on the heap - not in the arena with the components. With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will _not_ cascade to children if the child's props did not change. https://dmitripavlutin.com/use-react-memo-wisely/ -This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. +This behavior is defined as an attribute implicit to user components. When in React land you might wrap a component with `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. ```rust fn test() -> DomTree { @@ -40,7 +40,7 @@ fn test_component(cx: Scope, name: String) -> Element { "This is different than React, why differ?". -Take a component likes this: +Take a component like this: ```rust fn test(cx: Scope) -> DomTree { @@ -55,4 +55,4 @@ fn test(cx: Scope) -> DomTree { } ``` -While the contents of the destructured bundle might change, not every child component will need to be re-rendered. +While the contents of the destructured bundle might change, not every child component will need to be re-rendered every time the context changes. diff --git a/docs/guide/src/advanced-guides/12-signals.md b/docs/guide/src/advanced-guides/12-signals.md index 051247475..cae2e1726 100644 --- a/docs/guide/src/advanced-guides/12-signals.md +++ b/docs/guide/src/advanced-guides/12-signals.md @@ -1,6 +1,6 @@ # Signals: Skipping the Diff -In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. +In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the diffs. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. Signals build on the same infrastructure that powers asynchronous rendering where in-tree values can be updated outside of the render phase. In async rendering, a future is used as the signal source. With the raw signal API, any value can be used as a signal source. @@ -69,7 +69,7 @@ fn Calculator(cx: Scope) -> DomTree { } ``` -Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex that a basic operation (`std::ops`) we can either chain signals together or combine them: +Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex than a basic operation (`std::ops`) we can either chain signals together or combine them: ```rust let mut a = use_signal(&cx, || 0); @@ -88,7 +88,7 @@ let mut a = use_signal(&cx, || 0); let c = *a + *b; ``` -Calling `deref` or `derefmut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. +Calling `deref` or `deref_mut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. ## Global Signals @@ -128,7 +128,7 @@ Sometimes you want to use a collection of items. With Signals, you can bypass di By default, Dioxus is limited when you use iter/map. With the `For` component, you can provide an iterator and a function for the iterator to map to. -Dioxus automatically understands how to use your signals when mixed with iterators through Deref/DerefMut. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. +Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. ```rust const DICT: AtomFamily = |_| {}; diff --git a/docs/guide/src/advanced-guides/13-subtrees.md b/docs/guide/src/advanced-guides/13-subtrees.md index 37a2ef786..7c99ecf4f 100644 --- a/docs/guide/src/advanced-guides/13-subtrees.md +++ b/docs/guide/src/advanced-guides/13-subtrees.md @@ -6,7 +6,7 @@ For a VirtualDom that has a root tree with two subtrees, the edits follow a patt Root -> Tree 1 --> Tree 2 +-> Tree 2 -> Original root tree - Root edits @@ -39,7 +39,7 @@ fn Window() -> DomTree { onassign: move |e| { // create window } - children() + children() } } diff --git a/docs/guide/src/advanced-guides/custom-renderer.md b/docs/guide/src/advanced-guides/custom-renderer.md index fbff277b0..d2aa40bc9 100644 --- a/docs/guide/src/advanced-guides/custom-renderer.md +++ b/docs/guide/src/advanced-guides/custom-renderer.md @@ -20,7 +20,7 @@ For reference, check out the WebSys renderer as a starting point for your custom ## Trait implementation and DomEdits -The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here (but might not be up-to-date): +The current `RealDom` trait lives in `dioxus-core/diff`. A version of it is provided here (but might not be up-to-date): ```rust pub trait RealDom<'a> { diff --git a/docs/guide/src/async/index.md b/docs/guide/src/async/index.md index 41a8b9231..47b84e81a 100644 --- a/docs/guide/src/async/index.md +++ b/docs/guide/src/async/index.md @@ -1,13 +1,13 @@ # Working with Async -Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. +Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. So far, we've only talked about building apps with synchronous code, so this chapter will focus integrating asynchronous code into your app. ## The Runtime -By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. +By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/elements/component_children.md index d8469241a..d99273394 100644 --- a/docs/guide/src/elements/component_children.md +++ b/docs/guide/src/elements/component_children.md @@ -9,7 +9,7 @@ In this chapter, you'll learn about: ## The use case -Let's say you're building a user interface and want to make some part of it clickable to another website. You would normally start with the HTML `` tag, like so: +Let's say you're building a user interface and want to make some part of it a clickable link to another website. You would normally start with the HTML `` tag, like so: ```rust rsx!( @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn clickable(cx: Scope) -> Element { +fn Clickable(cx: Scope) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -162,7 +162,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { ..cx.props.attributes, "Any link, anywhere" } @@ -186,7 +186,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { onclick: move |evt| cx.props.onclick.call(evt) } )) diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md index 8567e9270..ea43034d9 100644 --- a/docs/guide/src/elements/components.md +++ b/docs/guide/src/elements/components.md @@ -3,8 +3,8 @@ In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components. In this chapter, we'll learn: -- What makes a Component -- How to model a component and its properties in Dioxus +- What makes a Component +- How to model a component and its properties in Dioxus - How to "think declaratively" ## What is a component? @@ -39,7 +39,7 @@ struct PostData { } ``` -If we look at the layout of the component, we notice quite a few buttons and functionality: +If we look at the layout of the component, we notice quite a few buttons and pieces of functionality: - Upvote/Downvote - View comments @@ -51,9 +51,9 @@ If we look at the layout of the component, we notice quite a few buttons and fun - Crosspost - Filter by site - View article -- Visit user +- Visit user -If we included all this functionality in one `rsx!` call, it would be huge! Instead, let's break the post down into some core pieces: +If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into some core pieces: ![Post as Component](../images/reddit_post_components.png) @@ -112,7 +112,7 @@ Let's take a look at the `VoteButton` component. For now, we won't include any i Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`. -As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. +As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. ```rust #[derive(PartialEq, Props)] @@ -133,9 +133,9 @@ fn VoteButton(cx: Scope) -> Element { ## Borrowing -You can avoid clones using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. +You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. -Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. +Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props. @@ -149,7 +149,7 @@ fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow. @@ -174,7 +174,7 @@ fn TitleCard(cx: Scope) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` to: @@ -185,7 +185,7 @@ fn TitleCard(cx: Scope, title: String) -> Element { cx.render(rsx!{ h1 { "{title}" } }) -} +} ``` Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. @@ -194,9 +194,9 @@ However, it's great for quickly throwing together an app without dealing with *a ## The `Scope` object -Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. +Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. -Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? +Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs. diff --git a/docs/guide/src/elements/conditional_rendering.md b/docs/guide/src/elements/conditional_rendering.md index 211da5d1a..c13263cf6 100644 --- a/docs/guide/src/elements/conditional_rendering.md +++ b/docs/guide/src/elements/conditional_rendering.md @@ -27,7 +27,7 @@ Now that we have a "logged_in" flag accessible in our props, we can render two d ```rust fn App(cx: Scope) -> Element { - if props.logged_in { + if cx.props.logged_in { cx.render(rsx!{ DashboardScreen {} }) @@ -39,7 +39,7 @@ fn App(cx: Scope) -> Element { } ``` -When the user is logged in, then this component will return the DashboardScreen. Else, the component will render the LoginScreen. +When the user is logged in, then this component will return the DashboardScreen. If they're not logged in, the component will render the LoginScreen. ## Using match statements diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/elements/exporting_components.md index 16d6f1002..117249db8 100644 --- a/docs/guide/src/elements/exporting_components.md +++ b/docs/guide/src/elements/exporting_components.md @@ -25,27 +25,27 @@ fn main() { dioxus::desktop::launch(App); } -fn App(Scope) -> Element {} +fn App(Scope) -> Element {} #[derive(PartialEq, Props)] struct PostProps{} -fn Post(Scope) -> Element {} +fn Post(Scope) -> Element {} #[derive(PartialEq, Props)] struct VoteButtonsProps {} -fn VoteButtons(Scope) -> Element {} +fn VoteButtons(Scope) -> Element {} #[derive(PartialEq, Props)] struct TitleCardProps {} -fn TitleCard(Scope) -> Element {} +fn TitleCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct MetaCardProps {} -fn MetaCard(Scope) -> Element {} +fn MetaCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` That's a lot of components for one file! We've successfully refactored our app into components, but we should probably start breaking it up into a file for each component. @@ -61,7 +61,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. @@ -104,10 +104,10 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` -If you tried to build this app right now, you'll get an error message saying that `Post is private, trying changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": +If you tried to build this app right now, you'll get an error message saying that `Post is private, try changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": ```rust // src/post/mod.rs @@ -116,7 +116,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct PostProps {} -pub fn Post(Scope) -> Element {} +pub fn Post(Scope) -> Element {} ``` While we're here, we also need to make sure each of our subcomponents are included as modules and exported. @@ -203,7 +203,7 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` @@ -255,7 +255,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct VoteButtonsProps {} -pub fn VoteButtons(Scope) -> Element {} +pub fn VoteButtons(Scope) -> Element {} ``` ```rust @@ -264,7 +264,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct TitleCardProps {} -pub fn TitleCard(Scope) -> Element {} +pub fn TitleCard(Scope) -> Element {} ``` ```rust @@ -273,7 +273,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct MetaCardProps {} -pub fn MetaCard(Scope) -> Element {} +pub fn MetaCard(Scope) -> Element {} ``` ```rust @@ -282,7 +282,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct ActionCardProps {} -pub fn ActionCard(Scope) -> Element {} +pub fn ActionCard(Scope) -> Element {} ``` ## Moving forward diff --git a/docs/guide/src/elements/index.md b/docs/guide/src/elements/index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/elements/index.md +++ b/docs/guide/src/elements/index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 32dbfd833..37e87b7da 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -1,6 +1,6 @@ # Conditional Lists and Keys -You will often want to display multiple similar components from a collection of data. +You will often want to display multiple similar components from a collection of data. In this chapter, you will learn: @@ -22,9 +22,9 @@ rsx!( ) ``` -Instead, we need to transform the list of data into a list of Elements. +Instead, we need to transform the list of data into a list of Elements. -For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. +For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. As a simple example, let's render a list of names. First, start with our input data: @@ -61,7 +61,7 @@ The HTML-rendered version of this list would follow what you would expect: ### Rendering our posts with a PostList component -Let's start by modeling this problem with a component and some properties. +Let's start by modeling this problem with a component and some properties. For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. @@ -98,7 +98,7 @@ fn App(cx: Scope) -> Element { Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. -As a very simple example, let's set up a filter where we only list names that begin with the letter "J". +As a very simple example, let's set up a filter where we only list names that begin with the letter "J". Let's make our list of names: @@ -117,7 +117,7 @@ let name_list = names Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce. -For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. +For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index b407ed02f..d441693d7 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -93,7 +93,7 @@ Alternatively, `&str` can be included directly, though it must be inside of an a rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) ``` -This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. +This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. Typically, with Dioxus, you'll just want to compute your substrings outside of the `rsx!` call and leverage the f-string formatting: @@ -138,11 +138,11 @@ rsx!( All element attributes must occur *before* child elements. The `rsx!` macro will throw an error if your child elements come before any of your attributes. If you don't see the error, try editing your Rust-Analyzer IDE setting to ignore macro-errors. This is a temporary workaround because Rust-Analyzer currently throws *two* errors instead of just the one we care about. ```rust -// settings.json +// settings.json { "rust-analyzer.diagnostics.disabled": [ "macro-error" - ], + ], } ``` @@ -166,7 +166,7 @@ This chapter just scratches the surface on how Elements can be defined. We learned: - Elements are the basic building blocks of User Interfaces -- Elements can contain other elements +- Elements can contain other elements - Elements can either be a named container or text - Some Elements have properties that the renderer can use to draw the UI to the screen diff --git a/docs/guide/src/final.md b/docs/guide/src/final.md index 68706f1aa..41449edc7 100644 --- a/docs/guide/src/final.md +++ b/docs/guide/src/final.md @@ -15,7 +15,7 @@ With any luck, you followed through the "Putting it All Together" mini guide and Continuing on your journey with Dioxus, you can try a number of things: -- Build a simple TUI app +- Build a simple TUI app - Publish your search engine app - Deploy a WASM app to GitHub - Design a custom hook @@ -37,7 +37,7 @@ The core team is actively working on: - Declarative window management (via Tauri) for Desktop apps - Portals for Dioxus Core -- Mobile support +- Mobile support - Integration with 3D renderers - Better async story (suspense, error handling) - Global state management diff --git a/docs/guide/src/hello_world.md b/docs/guide/src/hello_world.md index e9eb92081..e1d4777d9 100644 --- a/docs/guide/src/hello_world.md +++ b/docs/guide/src/hello_world.md @@ -1,6 +1,6 @@ # "Hello, World" desktop app -Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. +Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. In this chapter, we'll cover: @@ -31,7 +31,7 @@ $ tree We are greeted with a pre-initialized git repository, our code folder (`src`) and our project file (`Cargo.toml`). -Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is ran. +Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is run. ```shell $ more src/main.rs @@ -128,13 +128,13 @@ Finally, our app. Every component in Dioxus is a function that takes in `Context fn App(cx: Scope) -> Element { cx.render(rsx! { div { "Hello, world!" } - }) + }) } ``` ### What is this `Scope` object? -Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. +Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. In Dioxus, you are given an explicit `Scope` object to control how the component renders and stores data. The `Scope` object provides a handful of useful APIs for features like suspense, rendering, and more. diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index 029e2db15..adf43c6ef 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -1,5 +1,5 @@ # Hooks and Internal State - + In the [Adding Interactivity](./interactivity.md) section, we briefly covered the concept of hooks and state stored internal to components. In this section, we'll dive a bit deeper into hooks, exploring both the theory and mechanics. @@ -10,7 +10,7 @@ In this section, we'll dive a bit deeper into hooks, exploring both the theory a Over the past several decades, computer scientists and engineers have long sought the "right way" of designing user interfaces. With each new programming language, novel features are unlocked that change the paradigm in which user interfaces are coded. -Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. +Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. Broadly, there are two types of GUI structures: @@ -21,7 +21,7 @@ Typically, immediate-mode GUIs are simpler to write but can slow down as more fe Many GUIs today are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered. To help accommodate retained mode GUIs, like the web browser, Dioxus provides a mechanism to keep state around. -> Note: Even though hooks are accessible, you should still prefer to one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. +> Note: Even though hooks are accessible, you should still prefer one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. ## Mechanics of Hooks In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function. @@ -48,7 +48,7 @@ fn example(cx: Scope) -> Element { } ``` -Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. +Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. ```rust fn example(cx: Scope) -> Element { @@ -182,7 +182,7 @@ By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free - [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context` For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference. - + ## Wrapping up In this chapter, we learned about the mechanics and intricacies of storing state inside a component. diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 9e23862f1..0e1a61252 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -10,7 +10,7 @@ Before we get too deep into the mechanics of interactivity, we should first unde Every app you'll ever build has some sort of information that needs to be rendered to the screen. Dioxus is responsible for translating your desired user interface to what is rendered to the screen. *You* are responsible for providing the content. -The dynamic data in your user interface is called `State`. +The dynamic data in your user interface is called `State`. When you first launch your app with `dioxus::web::launch_with_props` you'll be providing the initial state. You need to declare the initial state *before* starting the app. @@ -32,7 +32,7 @@ fn main() { } ``` -When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children. +When Dioxus renders your app, it will pass an immutable reference to `PostProps` into your `Post` component. Here, you can pass the state down into children. ```rust fn App(cx: Scope) -> Element { @@ -73,14 +73,14 @@ fn App(cx: Scope)-> Element { url: String::from("dioxuslabs.com"), title: String::from("Hello, world"), original_poster: String::from("dioxus") - } + } }); cx.render(rsx!{ Title { title: &post.title } Score { score: &post.score } // etc - }) + }) } ``` @@ -102,7 +102,7 @@ We'll dive deeper into how exactly these hooks work later. ### When do I update my state? -There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. +There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. ### Updating state in listeners @@ -120,7 +120,7 @@ fn App(cx: Scope)-> Element { "Generate a random post" } Post { props: &post } - }) + }) } ``` @@ -162,7 +162,7 @@ Whenever you inform Dioxus that the component needs to be updated, it will "rend ![Diffing](../images/diffing.png) -In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. +In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. With these building blocks, we can craft new hooks similar to `use_state` that let us easily tell Dioxus that new information is ready to be sent to the screen. diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 223150521..de634dc0a 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -1,6 +1,6 @@ # Overview -In this chapter, we're going to get "set up" with a small desktop application. +In this chapter, we're going to get set up with a small desktop application. We'll learn about: - Installing the Rust programming language @@ -15,16 +15,15 @@ For platform-specific guides, check out the [Platform Specific Guides](../platfo Dioxus requires a few main things to get up and running: - The [Rust compiler](https://www.rust-lang.org) and associated build tooling - - An editor of your choice, ideally configured with the [Rust-Analyzer LSP plugin](https://rust-analyzer.github.io) Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more. ### Installing Rust -Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. +Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. -Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. +Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. ``` rustup target add wasm32-unknown-unknown @@ -32,7 +31,7 @@ rustup target add wasm32-unknown-unknown ### Platform-Specific Dependencies -If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of linux with no default web rendering engine, you might need to install some additional dependencies. +If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of Linux with no default web rendering engine, you might need to install some additional dependencies. For windows users: download the [bootstrapper for Webview2 from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) @@ -49,13 +48,13 @@ When distributing onto older Windows platforms or less-mainstream ### Dioxus-CLI for dev server, bundling, etc. -We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (should be automatically installed with Rust): +We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (which should be automatically installed with Rust): ``` $ cargo install dioxus-cli ``` -You can update the dioxus-cli at any time with: +You can update dioxus-cli at any time with: ``` $ cargo install --force dioxus-cli diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 9b3e054fc..1874dd73e 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -2,7 +2,7 @@ Every app you'll build with Dioxus will have some sort of state that needs to be maintained and updated as your users interact with it. However, managing state can be particular challenging at times, and is frequently the source of bugs in many GUI frameworks. -In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview +In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview -## Terminology +## Terminology diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..7099311ce 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -81,7 +81,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// // Using an "ID" associated with your data is a good idea. /// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) /// }} -/// +/// /// // Matching /// {match true { /// true => rsx!(h1 {"Top text"}), @@ -229,18 +229,18 @@ pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStrea /// #[inline_props] /// fn app(cx: Scope, bob: String) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// /// // is equivalent to /// /// #[derive(PartialEq, Props)] /// struct AppProps { /// bob: String, -/// } +/// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// ``` #[proc_macro_attribute] pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/core/README.md b/packages/core/README.md index 6313b1415..0cb185f45 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -94,5 +94,5 @@ Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinn If you don't want to use a crate that uses unsafe, then this crate is not for you. -However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous variants required to execute certain features. +However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features. diff --git a/packages/core/architecture.md b/packages/core/architecture.md index f852fda3c..87c3cf4a6 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -5,7 +5,7 @@ This document is mostly a brain-dump on how things work. A lot of this informati Main topics covered here: - Fiber, Concurrency, and Cooperative Scheduling - Suspense -- Signals +- Signals - Patches - Diffing - Const/Static structures @@ -25,7 +25,7 @@ During diffing, the "caller" closure is updated if the props are not `static. Th Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets. -The Dioxus hook model uses a Bump arena where user's data lives. +The Dioxus hook model uses a Bump arena where user's data lives. Initializing hooks: - The component is created @@ -38,7 +38,7 @@ Initializing hooks: Running hooks: - Each time use_hook is called, the internal hook state is fetched as &mut T -- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies +- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies - The hook counter is incremented @@ -62,7 +62,7 @@ The diffing engine in Dioxus expects the RealDom Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries. -There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. +There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. @@ -70,11 +70,11 @@ There are no contractual obligations between the VirtualDOM and RealDOM. When th When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work. -While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. +While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. -Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. +Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. -All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. +All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. ```js // returns a "deadline" object function idle() { @@ -83,7 +83,7 @@ function idle() { ``` ## Suspense -In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. +In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized. diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index c0dad0a47..8386cedd8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -18,7 +18,7 @@ impl BubbleState { } } -/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. +/// User Events are events that are shuttled from the renderer into the VirtualDom through the scheduler channel. /// /// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree /// where each listener is checked and fired if the event name matches. diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 80de92917..e30a07001 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -21,7 +21,7 @@ use std::{ /// - the `rsx!` macro /// - the [`NodeFactory`] API pub enum VNode<'src> { - /// Text VNodes simply bump-allocated (or static) string slices + /// Text VNodes are simply bump-allocated (or static) string slices /// /// # Example /// @@ -399,7 +399,7 @@ impl

AnyProps for VComponentProps

{ } // Safety: - // this will downcat the other ptr as our swallowed type! + // this will downcast the other ptr as our swallowed type! // you *must* make this check *before* calling this method // if your functions are not the same, then you will downcast a pointer into a different type (UB) unsafe fn memoize(&self, other: &dyn AnyProps) -> bool { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index cfc555543..bfa721f69 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -70,7 +70,7 @@ impl ScopeArena { } /// Safety: - /// - Obtaining a mutable refernece to any Scope is unsafe + /// - Obtaining a mutable reference to any Scope is unsafe /// - Scopes use interior mutability when sharing data into components pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { unsafe { self.scopes.borrow().get(&id).map(|f| &**f) } @@ -101,7 +101,7 @@ impl ScopeArena { let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); /* - This scopearena aggressively reuse old scopes when possible. + This scopearena aggressively reuses old scopes when possible. We try to minimize the new allocations for props/arenas. However, this will probably lead to some sort of fragmentation. diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index f29a13e17..b9640792d 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -3,7 +3,7 @@ use crate::innerlude::*; pub struct ElementIdIterator<'a> { vdom: &'a VirtualDom, - // Heuristcally we should never bleed into 5 completely nested fragments/components + // Heuristically we should never bleed into 5 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 29e8355cc..4e426caad 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,12 +9,12 @@ use fxhash::FxHashSet; use indexmap::IndexSet; use std::{collections::VecDeque, iter::FromIterator, task::Poll}; -/// A virtual node s ystem that progresses user events and diffs UI trees. +/// A virtual node system that progresses user events and diffs UI trees. /// /// /// ## Guide /// -/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. +/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. /// /// ```rust, ignore /// #[derive(Props, PartialEq)] @@ -233,7 +233,7 @@ impl VirtualDom { /// Get the [`Scope`] for the root component. /// - /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus + /// This is useful for traversing the tree from the root for heuristics or alternative renderers that use Dioxus /// directly. /// /// This method is equivalent to calling `get_scope(ScopeId(0))` @@ -618,7 +618,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Base); /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` + /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); machine.stack.push(DiffInstruction::Diff { new, old }); diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs index 2638feaa8..83acf781e 100644 --- a/packages/hooks/src/usestate/handle.rs +++ b/packages/hooks/src/usestate/handle.rs @@ -66,7 +66,7 @@ impl<'a, T: 'static> UseState<'a, T> { }) } - pub fn wtih(&self, f: impl FnOnce(&mut T)) { + pub fn with(&self, f: impl FnOnce(&mut T)) { let mut val = self.0.wip.borrow_mut(); if let Some(inner) = val.as_mut() { diff --git a/packages/html/README.md b/packages/html/README.md index 46f1b2078..cbc03b0f2 100644 --- a/packages/html/README.md +++ b/packages/html/README.md @@ -42,7 +42,7 @@ The HTML namespace is defined mostly with macros. However, the expanded form wou struct base; impl DioxusElement for base { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } impl base { #[inline] @@ -60,7 +60,7 @@ Because attributes are defined as methods on the unit struct, they guard the att ## How to extend it: -Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. +Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. ```rust mod dioxus_elements { @@ -68,13 +68,13 @@ mod dioxus_elements { struct my_element; impl DioxusElement for my_element { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } } ``` ## Limitations: -- +- ## How to work around it: If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do. @@ -87,19 +87,19 @@ cx.render(rsx!{ {LazyNodes::new(move |f| { f.raw_element( // tag name - "custom_element", + "custom_element", // attributes - &[f.attr("billy", format_args!("goat"))], + &[f.attr("billy", format_args!("goat"))], // listeners - &[f.listener(onclick(move |_| {}))], + &[f.listener(onclick(move |_| {}))], // children - &[cx.render(rsx!(div {} ))], + &[cx.render(rsx!(div {} ))], // key - None + None ) })} } diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 1d708eab2..00a8ce2d6 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -20,7 +20,7 @@ async fn main() -> tide::Result<()> { } ``` -Dioxus LiveView runs your Dioxus apps on the server +Dioxus LiveView runs your Dioxus apps on the server @@ -36,7 +36,7 @@ async fn main() { async fn order_shoes(mut req: WebsocketRequest) -> Response { let stream = req.upgrade(); - dioxus::liveview::launch(App, stream).await; + dioxus::liveview::launch(App, stream).await; } fn App(cx: Scope) -> Element { diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 5a41ffe60..aa3470761 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo mobile init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/packages/router/README.md b/packages/router/README.md index c32441888..a6dc5027b 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -16,7 +16,7 @@ fn app() { Then, in your route, you can choose to parse the Route any way you want through `use_route`. ```rust -let id: usize = use_route(&cx).path("id")?; +let id: usize = use_route(&cx).segment("id")?; let state: CustomState = use_route(&cx).parse()?; ``` diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 6a1b671ab..4c0ecb996 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -11,7 +11,7 @@ pub struct LinkProps<'a> { /// The url that gets pushed to the history stack /// - /// You can either put it your own inline method or just autoderive the route using `derive(Routable)` + /// You can either put in your own inline method or just autoderive the route using `derive(Routable)` /// /// ```rust, ignore /// diff --git a/packages/ssr/README.md b/packages/ssr/README.md index d6c199fc8..7a7f1c4be 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -52,7 +52,7 @@ let content = dioxus::ssr::render_vdom(&dom); ``` ## Configuring output -It's possible to configure the output of the generated HTML. +It's possible to configure the output of the generated HTML. ```rust, ignore let content = dioxus::ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true)); @@ -82,7 +82,7 @@ buf.write_fmt!(format_args!("{}", args)); -## Usage in pre-rendering +## Usage in pre-rendering This crate is particularly useful in pre-generating pages server-side and then selectively loading dioxus client-side to pick up the reactive elements. diff --git a/src/lib.rs b/src/lib.rs index c0a984abb..d7b479a07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! //! # Resources //! -//! This overview is provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: +//! This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: //! - [Getting Started](https://dioxuslabs.com/getting-started) //! - [Book](https://dioxuslabs.com/book) //! - [Reference](https://dioxuslabs.com/reference) @@ -53,7 +53,7 @@ //! //! ## Elements & your first component //! -//! To assemble UI trees with Diouxs, you need to use the `render` function on +//! To assemble UI trees with Dioxus, you need to use the `render` function on //! something called `LazyNodes`. To produce `LazyNodes`, you can use the `rsx!` //! macro or the NodeFactory API. For the most part, you want to use the `rsx!` //! macro. @@ -74,7 +74,7 @@ //! ) //! ``` //! -//! The rsx macro accepts attributes in "struct form" and then will parse the rest +//! The `rsx!` macro accepts attributes in "struct form" and will parse the rest //! of the body as child elements and rust expressions. Any rust expression that //! implements `IntoIterator` will be parsed as a child. //! @@ -87,7 +87,7 @@ //! //! ``` //! -//! Used within components, the rsx! macro must be rendered into an `Element` with +//! Used within components, the `rsx!` macro must be rendered into an `Element` with //! the `render` function on Scope. //! //! If we want to omit the boilerplate of `cx.render`, we can simply pass in @@ -104,7 +104,7 @@ //! } //! ``` //! -//! Putting everything together, we can write a simple component that a list of +//! Putting everything together, we can write a simple component that renders a list of //! elements: //! //! ```rust, ignore @@ -146,8 +146,8 @@ //! } //! ``` //! -//! Our `Header` component takes in a `title` and a `color` property, which we -//! delcare on an explicit `HeaderProps` struct. +//! Our `Header` component takes a `title` and a `color` property, which we +//! declare on an explicit `HeaderProps` struct. //! //! ```rust, ignore //! // The `Props` derive macro lets us add additional functionality to how props are interpreted. @@ -203,8 +203,8 @@ //! } //! ``` //! -//! Components that beging with an uppercase letter may be called through -//! traditional curly-brace syntax like so: +//! Components that begin with an uppercase letter may be called with +//! the traditional (for React) curly-brace syntax like so: //! //! ```rust, ignore //! rsx!( @@ -224,7 +224,7 @@ //! ## Hooks //! //! While components are reusable forms of UI elements, hooks are reusable forms -//! of logic. Hooks provide us a way of retrieving state from `Scope` and using +//! of logic. Hooks provide us a way of retrieving state from the `Scope` and using //! it to render UI elements. //! //! By convention, all hooks are functions that should start with `use_`. We can @@ -245,7 +245,7 @@ //! - Functions with "use_" should not be called in loops or conditionals //! //! In a sense, hooks let us add a field of state to our component without declaring -//! an explicit struct. However, this means we need to "load" the struct in the right +//! an explicit state struct. However, this means we need to "load" the struct in the right //! order. If that order is wrong, then the hook will pick the wrong state and panic. //! //! Most hooks you'll write are simply composition of other hooks: @@ -256,8 +256,8 @@ //! users.get(&id).map(|user| user.logged_in).ok_or(false) //! } //! ``` -//! -//! To create entirely new foundational hooks, we can use the `use_hook` method on ScopeState. +//! +//! To create entirely new foundational hooks, we can use the `use_hook` method on `ScopeState`. //! //! ```rust, ignore //! fn use_mut_string(cx: &ScopeState) -> &mut String { @@ -316,9 +316,9 @@ //! //! Alternatives to Dioxus include: //! - Yew: supports function components and web, but no SSR, borrowed data, or bump allocation. Rather slow at times. -//! - Percy: supports function components, web, ssr, but lacks in state management +//! - Percy: supports function components, web, ssr, but lacks state management //! - Sycamore: supports function components, web, ssr, but closer to SolidJS than React -//! - MoonZoom/Seed: opionated in the Elm model (message, update) - no hooks +//! - MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update) - no hooks //! //! We've put a lot of work into making Dioxus ergonomic and *familiar*. //! Our target audience is TypeSrcipt developers looking to switch to Rust for the web - so we need to be comparabale to React. From 5f4a72446e63441628e18bb19fd9b2d9f65f9d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20L=C3=B6vegren?= Date: Sat, 22 Jan 2022 19:57:46 +0200 Subject: [PATCH 040/244] Add gap and row_gap to style_trait_methods --- packages/html/src/global_attributes.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index c40eabd31..f99d041b5 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -399,6 +399,9 @@ pub trait GlobalAttributes { /// Specify the font weight of the text. font_weight: "font-weight", + /// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap. + gap: "gap", + /// Specify the height of an element. height: "height", @@ -530,6 +533,9 @@ pub trait GlobalAttributes { /// Specify the location of the right edge of the positioned element. right: "right", + + /// Specifies the gap between the rows in a multi_column element. + row_gap: "row-gap", /// Specifies the length of the tab character. tab_size: "tab-size", From f66d17ca84f80f199e5ce1735a11f087af7fe4ea Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 14:53:59 -0500 Subject: [PATCH 041/244] 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 43e78d56f72e23ada939f5688a6bd2fdd9b8292d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 14:55:19 -0500 Subject: [PATCH 042/244] fix: rustfmt --- packages/html/src/global_attributes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index f99d041b5..1368f87e3 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -533,7 +533,7 @@ pub trait GlobalAttributes { /// Specify the location of the right edge of the positioned element. right: "right", - + /// Specifies the gap between the rows in a multi_column element. row_gap: "row-gap", From 564bce8eeb32415583dce6d08216eba561b7997c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 16:19:52 -0500 Subject: [PATCH 043/244] docs: add azul compairson --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7b277d16a..262270dd5 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ Dioxus primarily emphasizes **developer experience** and **familiarity with Reac - [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. +- [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr # Parity with React From d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 02:34:16 -0500 Subject: [PATCH 044/244] fix: provide_root_context on root scopes --- packages/core/src/scopes.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index bfa721f69..4eda40c50 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -695,6 +695,7 @@ impl ScopeState { .insert(TypeId::of::(), value.clone()) .map(|f| f.downcast::().ok()) .flatten(); + return value; } let mut search_parent = self.parent_scope; From 1406c9020bb3102635737dc2d13e5b3171499a18 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 02:57:56 -0500 Subject: [PATCH 045/244] feat: add panic hook by default --- packages/web/Cargo.toml | 19 ++++--------------- packages/web/src/lib.rs | 4 ++++ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 52a78e631..a5befe7b3 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -20,7 +20,7 @@ wasm-bindgen-futures = "0.4.20" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" wasm-logger = "0.2.0" -console_error_panic_hook = "0.1.6" +console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.21" once_cell = "1.8" async-channel = "1.6.1" @@ -69,22 +69,11 @@ features = [ "IdleDeadline", ] - -# [lib] -# crate-type = ["cdylib", "rlib"] +[features] +default = ["panic_hook"] +panic_hook = ["console_error_panic_hook"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.28" dioxus-ssr = { path = "../ssr" } -# im-rc = "15.0.0" -# separator = "0.4.1" -# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } -# serde = { version = "1.0.126", features = ["derive"] } -# reqwest = { version = "0.11", features = ["json"] } -# dioxus-hooks = { path = "../hooks" } -# rand = { version = "0.8.4", features = ["small_rng"] } - -# [dev-dependencies.getrandom] -# version = "0.2" -# features = ["js"] diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8c0c0c3b4..e1e2af491 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -120,6 +120,10 @@ pub fn launch_with_props( T: Send + 'static, F: FnOnce(WebConfig) -> WebConfig, { + if cfg!(feature = "panic_hook") { + console_error_panic_hook::set_once(); + } + let config = configuration_builder(WebConfig::default()); wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } From 1c6d6421dc4c285751fda8666c1e400021006fdb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 03:01:04 -0500 Subject: [PATCH 046/244] 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 2e4f7659327b69b74e20af59b6d5e65c699b6adb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 12:21:24 -0500 Subject: [PATCH 047/244] tweak: drop hooks before resetting bump arena --- packages/core/src/scopes.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 4eda40c50..c54918b55 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -891,13 +891,15 @@ impl ScopeState { self.frames[0].reset(); self.frames[1].reset(); - // Finally, free up the hook values - self.hook_arena.reset(); + // Free up the hook values self.hook_vals.get_mut().drain(..).for_each(|state| { let as_mut = unsafe { &mut *state }; let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; drop(boxed); }); + + // Finally, clear the hook arena + self.hook_arena.reset(); } } From bad4b773b7bbab6a38d4f48d88d8c5a8787927ac Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 15:21:56 -0500 Subject: [PATCH 048/244] feat: add "spawn" method --- packages/core/src/scopes.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index c54918b55..77c648d18 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -751,6 +751,11 @@ impl ScopeState { self.tasks.push_fut(fut) } + /// Spawns the future but does not return the TaskId + pub fn spawn(&self, fut: impl Future + 'static) { + self.push_future(fut); + } + // todo: attach some state to the future to know if we should poll it pub fn remove_future(&self, id: TaskId) { self.tasks.remove_fut(id); From 26ada2c7c18097fe86579b71b8b1a20f4227e405 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 19:52:12 -0500 Subject: [PATCH 049/244] chore: make clippy happier --- examples/borrowed.rs | 2 ++ examples/calculator.rs | 6 +++--- examples/crm.rs | 2 +- examples/dog_app.rs | 5 +++-- examples/file_explorer.rs | 6 +++--- examples/filedragdrop.rs | 2 +- examples/framework_benchmark.rs | 4 +++- examples/inputs.rs | 2 -- examples/pattern_model.rs | 12 +++++++----- examples/router.rs | 12 ++++++++++-- examples/suspense.rs | 2 ++ examples/tailwind.rs | 2 ++ examples/tasks.rs | 11 +++++------ examples/todomvc.rs | 28 +++++++++++++--------------- 14 files changed, 55 insertions(+), 41 deletions(-) diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 63191bc97..815d6ceed 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + /* Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T. diff --git a/examples/calculator.rs b/examples/calculator.rs index a849833a3..8597a8897 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -98,7 +98,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push_str("."), + button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -175,7 +175,7 @@ fn calc_val(val: String) -> f64 { for c in val[start_index..].chars() { if c == '+' || c == '-' || c == '*' || c == '/' { - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), @@ -191,7 +191,7 @@ fn calc_val(val: String) -> f64 { } } - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), diff --git a/examples/crm.rs b/examples/crm.rs index d4f3d1579..504459fc7 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -1,7 +1,7 @@ /* Tiny CRM: A port of the Yew CRM example to Dioxus. */ -use dioxus::{events::FormEvent, prelude::*}; +use dioxus::prelude::*; fn main() { dioxus::desktop::launch(app); diff --git a/examples/dog_app.rs b/examples/dog_app.rs index b2f8429ee..f2c51d6fb 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -1,5 +1,6 @@ +#![allow(non_snake_case)] + //! Render a bunch of doggos! -//! use std::collections::HashMap; @@ -50,7 +51,7 @@ fn app(cx: Scope) -> Element { } } }), - Some(Err(e)) => cx.render(rsx! { + Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), None => cx.render(rsx! { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 5d4bf8be8..fe5ab23bb 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -15,7 +15,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let files = use_ref(&cx, || Files::new()); + let files = use_ref(&cx, Files::new); rsx!(cx, div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } @@ -28,8 +28,8 @@ fn app(cx: Scope) -> Element { } main { files.read().path_names.iter().enumerate().map(|(dir_id, path)| { - let path_end = path.split('/').last().unwrap_or(path.as_str()); - let icon_type = if path_end.contains(".") { + let path_end = path.split('/').last().unwrap_or_else(|| path.as_str()); + let icon_type = if path_end.contains('.') { "description" } else { "folder" diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index a36b64ddb..523405b39 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; fn main() { dioxus::desktop::launch_with_props(app, (), |c| { - c.with_file_drop_handler(|w, e| { + c.with_file_drop_handler(|_w, e| { println!("{:?}", e); false }) diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 0a70bd25b..69e3b8faa 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use dioxus::prelude::*; use rand::prelude::*; @@ -30,7 +32,7 @@ impl Label { } fn app(cx: Scope) -> Element { - let items = use_ref(&cx, || vec![]); + let items = use_ref(&cx, Vec::new); let selected = use_state(&cx, || None); cx.render(rsx! { diff --git a/examples/inputs.rs b/examples/inputs.rs index 11334b59a..5e0a52244 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -2,8 +2,6 @@ //! //! There is some conversion happening when input types are checkbox/radio/select/textarea etc. -use std::sync::Arc; - use dioxus::{events::FormEvent, prelude::*}; fn main() { diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index c679d1427..a7b821750 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Calculator //! ------------------- //! @@ -30,7 +32,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_ref(&cx, || Calculator::new()); + let state = use_ref(&cx, Calculator::new); cx.render(rsx! { style { [include_str!("./assets/calculator.css")] } @@ -171,8 +173,8 @@ impl Calculator { } } fn input_dot(&mut self) { - if self.display_value.find(".").is_none() { - self.display_value.push_str("."); + if !self.display_value.contains('.') { + self.display_value.push('.'); } } fn perform_operation(&mut self) { @@ -190,8 +192,8 @@ impl Calculator { } } fn toggle_sign(&mut self) { - if self.display_value.starts_with("-") { - self.display_value = self.display_value.trim_start_matches("-").to_string(); + if self.display_value.starts_with('-') { + self.display_value = self.display_value.trim_start_matches('-').to_string(); } else { self.display_value = format!("-{}", self.display_value); } diff --git a/examples/router.rs b/examples/router.rs index 121514423..f34676ab7 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -19,7 +19,7 @@ fn app(cx: Scope) -> Element { Route { to: "/", "Home" } Route { to: "users", Route { to: "/", "User list" } - Route { to: ":name", BlogPost {} } + Route { to: ":name", User {} } } Route { to: "blog" Route { to: "/", "Blog list" } @@ -48,12 +48,20 @@ struct Query { fn User(cx: Scope) -> Element { let post = dioxus::router::use_route(&cx).last_segment(); - let query = dioxus::router::use_route(&cx).query::(); + let query = dioxus::router::use_route(&cx) + .query::() + .unwrap_or(Query { bold: false }); cx.render(rsx! { div { h1 { "Reading blog post: {post}" } p { "example blog post" } + + if query.bold { + rsx!{ b { "bold" } } + } else { + rsx!{ i { "italic" } } + } } }) } diff --git a/examples/suspense.rs b/examples/suspense.rs index 881d6cae7..5a9bf4fd8 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Suspense in Dioxus //! //! Currently, `rsx!` does not accept futures as values. To achieve the functionality diff --git a/examples/tailwind.rs b/examples/tailwind.rs index cbefd248f..6611fc47b 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Basic Tailwind usage //! //! This example shows how an app might be styled with TailwindCSS. diff --git a/examples/tasks.rs b/examples/tasks.rs index a0ab7675c..f71abe38d 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -13,13 +13,12 @@ fn app(cx: Scope) -> Element { let count = use_state(&cx, || 0); use_future(&cx, move || { - let count = UseState::for_async(&count); - // for_async![count]; + let mut count = count.for_async(); async move { - // loop { - // tokio::time::sleep(Duration::from_millis(1000)).await; - // count += 1; - // } + loop { + tokio::time::sleep(Duration::from_millis(1000)).await; + count += 1; + } } }); diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8adb01d34..6daddbf20 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,7 +19,7 @@ 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); @@ -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(), + }, + ); + 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" } )) From 1d964c54e5266e820d57019064c92558f35bc1cc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 19:54:27 -0500 Subject: [PATCH 050/244] chore: fix cargo fmt --- examples/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/router.rs b/examples/router.rs index f34676ab7..216b69922 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -56,7 +56,7 @@ fn User(cx: Scope) -> Element { div { h1 { "Reading blog post: {post}" } p { "example blog post" } - + if query.bold { rsx!{ b { "bold" } } } else { From a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 20:24:50 -0500 Subject: [PATCH 051/244] docs: fix web doc example and use &mut for builders everywhere --- packages/desktop/src/lib.rs | 2 +- packages/web/src/lib.rs | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index bc9a84e42..93c175cc6 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -149,7 +149,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::new(); + let mut cfg = DesktopConfig::default(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e1e2af491..f4266a7c8 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -92,6 +92,29 @@ pub fn launch(root_component: Component) { launch_with_props(root_component, (), |c| c); } +/// Launch your app and run the event loop, with configuration. +/// +/// This function will start your web app on the main web thread. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn main() { +/// dioxus_web::launch_with_props(App, |config| config.pre_render(true)); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) { + launch_with_props(root, (), config_builder) +} + /// Launches the VirtualDOM from the specified component function and props. /// /// This method will block the thread with `spawn_local` @@ -100,7 +123,11 @@ pub fn launch(root_component: Component) { /// /// ```rust, ignore /// fn main() { -/// dioxus_web::launch_with_props(App, RootProps { name: String::from("joe") }); +/// dioxus_web::launch_with_props( +/// App, +/// RootProps { name: String::from("joe") }, +/// |config| config +/// ); /// } /// /// #[derive(ParitalEq, Props)] @@ -112,19 +139,19 @@ pub fn launch(root_component: Component) { /// rsx!(cx, div {"hello {cx.props.name}"}) /// } /// ``` -pub fn launch_with_props( +pub fn launch_with_props( root_component: Component, root_properties: T, - configuration_builder: F, + configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig, ) where T: Send + 'static, - F: FnOnce(WebConfig) -> WebConfig, { if cfg!(feature = "panic_hook") { console_error_panic_hook::set_once(); } - let config = configuration_builder(WebConfig::default()); + let mut config = WebConfig::default(); + configuration_builder(&mut config); wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } From ae676d9d816fcbf88d110157124443a5b3aef9eb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 20:25:32 -0500 Subject: [PATCH 052/244] fix: webconfig should take &mut self --- packages/web/src/cfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index cc5d18bf7..591868c34 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -30,7 +30,7 @@ impl WebConfig { /// work and suspended nodes. /// /// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded. - pub fn hydrate(mut self, f: bool) -> Self { + pub fn hydrate(&mut self, f: bool) -> &mut Self { self.hydrate = f; self } @@ -38,7 +38,7 @@ impl WebConfig { /// Set the name of the element that Dioxus will use as the root. /// /// This is akint to calling React.render() on the element with the specified name. - pub fn rootname(mut self, name: impl Into) -> Self { + pub fn rootname(&mut self, name: impl Into) -> &mut Self { self.rootname = name.into(); self } @@ -46,7 +46,7 @@ impl WebConfig { /// Set the name of the element that Dioxus will use as the root. /// /// This is akint to calling React.render() on the element with the specified name. - pub fn with_string_cache(mut self, cache: Vec) -> Self { + pub fn with_string_cache(&mut self, cache: Vec) -> &mut Self { self.cached_strings = cache; self } From 71371d570a82f136593c73ccdb4511369514a708 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 13:17:43 -0500 Subject: [PATCH 053/244] docs: change discord readme badge --- README.md | 3 ++- notes/README/ZH_CN.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 262270dd5..9895dfb76 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,12 @@ - Awesome Page + Discord Link +

Website diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 614774595..28bd07ed0 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -35,7 +35,7 @@ - Awesome Page + Discord Link

@@ -113,7 +113,7 @@ cargo run --example EXAMPLE ## Dioxus 项目 -| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | +| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | From 29ed7ebece26e9d53925af55f2f34a8fd8241405 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 15:06:37 -0500 Subject: [PATCH 054/244] 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 055/244] 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 06d81475f279e88c150ac319b7f7a1e213d3f145 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 00:40:01 -0500 Subject: [PATCH 056/244] docs: improve readme by removing table --- README.md | 61 ++++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9895dfb76..8ac24bc92 100644 --- a/README.md +++ b/README.md @@ -167,52 +167,31 @@ Dioxus primarily emphasizes **developer experience** and **familiarity with Reac - [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr -# Parity with React +## Parity with React & Roadmap -Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include important ecosystem crates like code blocks, markdown, resizing hooks, etc. +Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. These include: +- Portals +- Suspense integration with SSR +- Server Components / Bundle Splitting / Lazy -| Feature | Dioxus | React | Notes for Dioxus | -| ------------------------- | ------ | ----- | -------------------------------------------------------------------- | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Shared State (Context) | ✅ | ✅ | share state through the tree | -| Hooks | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Headless components | ✅ | ✅ | components that don't return real elements | -| Fragments | ✅ | ✅ | multiple elements without a real root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | -| Custom elements | ✅ | ✅ | Define new element primitives | -| Suspense | ✅ | ✅ | schedule future render from future/promise | -| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| NodeRef | ✅ | ✅ | gain direct access to nodes | -| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | -| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | -| Effects | ✅ | ✅ | Run effects after a component has been committed to render | -| Portals | 🛠 | ✅ | Render nodes outside of the traditional tree structure | -| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server | -| Bundle Splitting | 👀 | ✅ | Efficiently and asynchronously load the app | -| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded | -| 1st class global state | ✅ | ✅ | redux/recoil/mobx on top of context | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | -| High-efficiency templates | 🛠 | ❓ | rsx! calls are translated to templates on the DOM's side | -| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | -| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations | -| Fine-grained reactivity | 👀 | ❓ | Skip diffing for fine-grain updates | +Dioxus is unique in the Rust ecosystem in that it supports: -- ✅ = implemented and working -- 🛠 = actively being worked on -- 👀 = not yet implemented or being worked on -- ❓ = not sure if will or can implement +- Components with props that borrow from their parent +- Server-side-rendering with client-side hydration +- Support for desktop applications +For more information on what features are currently available and the roadmap for the future, be sure to check out [the guide](https://dioxuslabs.com/guide/). + +## Projects in the ecosystem + +Want to jump in and help build the future of Rust frontend? There's plenty of places where your contributions can make a huge difference: + +- [TUI renderer](https://github.com/dioxusLabs/rink) +- [CLI Tooling](https://github.com/dioxusLabs/cli) +- [Documentation and Example Projects](https://github.com/dioxusLabs/docsite) +- LiveView and Web Server +- Asset System ## License From fbb610df329d83357c9e03faf6a11d24bbf20ef7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 00:40:51 -0500 Subject: [PATCH 057/244] docs: change indentation on readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ac24bc92..658a6e48d 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Specifically, Dioxus provides us many other assurances: And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. -### Why NOT Dioxus? +## Why NOT Dioxus? You shouldn't use Dioxus if: - You don't like the React Hooks approach to frontend @@ -157,7 +157,7 @@ You shouldn't use Dioxus if: - You want to support browsers where Wasm or asm.js are not supported. - You need a Send+Sync UI solution (Dioxus is not currently thread-safe) -### Comparison with other Rust UI frameworks +## Comparison with other Rust UI frameworks Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. - [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). From 25286a6711e2505601f34c39bb8166d793dc927e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 14:42:07 -0500 Subject: [PATCH 058/244] 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 059/244] 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 060/244] 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 061/244] 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 062/244] 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 063/244] 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 064/244] 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 065/244] 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 066/244] 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 067/244] 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 068/244] 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 069/244] 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 070/244] 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 071/244] 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 072/244] 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 073/244] 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 074/244] 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 075/244] 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 076/244] 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 077/244] 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 078/244] 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 079/244] 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 080/244] 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 081/244] 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 082/244] 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 083/244] 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 084/244] 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 085/244] 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 086/244] 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 087/244] 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 088/244] 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 089/244] 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 090/244] 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 091/244] 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 092/244] 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 093/244] 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 c72a90f42ee93d2e5c44a761e6702a22696a6ba9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:39:24 -0500 Subject: [PATCH 094/244] Release dioxus-web v0.0.5 From d570bb6c42c59ef76bee7f55be123bdbee688794 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 29 Jan 2022 10:40:03 -0500 Subject: [PATCH 095/244] 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 096/244] 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 097/244] 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 098/244] 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 099/244] 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 100/244] 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 101/244] 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 102/244] 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 103/244] 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 104/244] 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 105/244] 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 106/244] 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 107/244] 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 108/244] 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 109/244] 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 110/244] 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 111/244] 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 112/244] 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 113/244] 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 114/244] 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 115/244] 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 116/244] 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 117/244] 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 118/244] 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 119/244] 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 120/244] 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 121/244] 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 122/244] 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 123/244] 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 124/244] 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 125/244] 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 126/244] 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 127/244] 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 128/244] 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 129/244] 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 130/244] 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 131/244] 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 132/244] 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 133/244] 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 134/244] 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 135/244] 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 136/244] 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 137/244] 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 138/244] 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 139/244] 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 140/244] 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 141/244] 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 142/244] 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 143/244] 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 144/244] 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 145/244] 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 146/244] 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 147/244] 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 148/244] 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 149/244] 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 150/244] 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 151/244] 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 152/244] 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 153/244] 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 154/244] 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 155/244] 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 156/244] 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 157/244] 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 158/244] 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 159/244] 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 160/244] 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 161/244] 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 162/244] 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 163/244] 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 164/244] 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 165/244] 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 166/244] 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 167/244] 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 168/244] 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 169/244] 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 170/244] 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 171/244] 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 172/244] 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 173/244] 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 174/244] 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 175/244] 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 176/244] 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 177/244] 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 178/244] 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 179/244] 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 180/244] 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 181/244] 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 182/244] 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 183/244] 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 184/244] 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 185/244] 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 186/244] 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 187/244] 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 188/244] 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 189/244] 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 190/244] 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 191/244] 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 192/244] 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 193/244] 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 194/244] 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 195/244] 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 196/244] 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 197/244] 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 198/244] 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 199/244] 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 200/244] 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 201/244] 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 202/244] 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 203/244] 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 204/244] 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 205/244] 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 206/244] 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 207/244] 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 208/244] 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 209/244] 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 210/244] 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 211/244] 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 212/244] 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 213/244] 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 214/244] 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 215/244] 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 216/244] 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 217/244] 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 218/244] 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 219/244] 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 220/244] 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 221/244] 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 222/244] 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 223/244] 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 224/244] 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 225/244] 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 226/244] 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 227/244] 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 228/244] 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 229/244] 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 230/244] 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 231/244] 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 232/244] 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 233/244] 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 234/244] 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 235/244] 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 236/244] 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 237/244] 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 238/244] 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 239/244] 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 240/244] 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 241/244] 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 242/244] 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 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

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 243/244] 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 244/244] 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()) +// }