diff --git a/.vscode/.gitignore b/.vscode/.gitignore index e38da2008..e69de29bb 100644 --- a/.vscode/.gitignore +++ b/.vscode/.gitignore @@ -1 +0,0 @@ -settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a3b8a0b79 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.inlayHints.enable": false, + "rust-analyzer.cargo.allFeatures": true +} diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 348655db4..8f587cea4 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -66,3 +66,4 @@ constified SegVec contentful Jank +noderef diff --git a/Cargo.toml b/Cargo.toml index 043c7c802..abd76ec8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ dioxus-mobile = { path = "./packages/mobile", optional = true } [features] # core -default = ["core", "ssr"] +default = ["core", "ssr", "web"] core = ["macro", "hooks", "html"] macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] @@ -51,8 +51,10 @@ argh = "0.1.5" env_logger = "*" async-std = { version = "1.9.0", features = ["attributes"] } rand = { version = "0.8.4", features = ["small_rng"] } -surf = { version = "2.2.0", git = "https://github.com/jkelleyrtp/surf/", branch = "jk/fix-the-wasm" } gloo-timers = "0.2.1" +# surf = { version = "2.3.1", default-features = false, features = [ +# "wasm-client", +# ] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] gloo-timers = "0.2.1" diff --git a/README.md b/README.md index f1a6e02a1..f811045d6 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,38 @@
- - + Crates.io version - - + Download - + docs.rs docs - - + CI status

- + API Docs | - + Website | - + Examples

@@ -49,34 +49,21 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust. ```rust -fn App(cx: Context<()>) -> DomTree { +static App: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); - cx.render(rsx! { + cx.render(rsx!( h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } - }) + )) }; ``` -Dioxus can be used to deliver webapps, desktop apps, static pages, liveview apps, eventually mobile apps (WIP), and more. At its core, Dioxus is entirely renderer agnostic and has great documentation for creating new renderers for any platform. +Dioxus can be used to deliver webapps, desktop apps, static sites, liveview apps, mobile apps (WIP), and more. At its core, Dioxus is entirely renderer agnostic and has great documentation for creating new renderers for any platform. If you know React, then you already know Dioxus. -### **Things you'll love โค๏ธ:** - -- Ergonomic design -- Minimal boilerplate -- Simple build, test, and deploy -- Compile-time correct templating -- Support for fine-grained reactivity -- Support for html! and rsx! templates -- SSR, WASM, desktop, and mobile support -- Support for asynchronous batched rendering -- Powerful and simple integrated state management -- Rust! (enums, static types, modules, efficiency) - ### Unique features: - Incredible inline documentation. Supports hover and guides for all HTML elements, listeners, and events. - Templates are "constified" at compile time. Nodes that don't change will won't be diffed. @@ -100,8 +87,32 @@ If you know React, then you already know Dioxus. -## Explore +## Examples: +| File Navigator (Desktop) | Bluetooth scanner (Desktop) | TodoMVC (All platforms) | Widget Gallery | +| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | + + + +| E-Commerce Site | Doxie Documentation Generator | Chatroom | Widget Gallery | +| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | + + + +| Printer Demo | Slide Puzzle | Todo | Widget Gallery | +| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | ![asd](https://sixtyfps.io/resources/printerdemo_screenshot.png) | + + + + +See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem. + + ## Why? @@ -145,7 +155,6 @@ And much more. Dioxus makes Rust apps just as fast to write as React apps, but a 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. -### Phase 1: The Basics | Feature | Dioxus | React | Notes for Dioxus | | ------------------------- | ------ | ----- | -------------------------------------------------------------------- | @@ -178,30 +187,60 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Fine-grained reactivity | ๐Ÿ›  | โ“ | Skip diffing for fine-grain updates | | Effects | ๐Ÿ›  | โœ… | Run effects after a component has been committed to render | - -### Phase 2: Advanced Toolkits - -| Feature | Dioxus | React | Notes for Dioxus | -| --------------------- | ------ | ----- | ---------------------------------- | -| 1st class router | ๐Ÿ‘€ | โœ… | Hook built on top of history | -| Assets | ๐Ÿ‘€ | โœ… | include css/svg/img url statically | -| Integrated classnames | ๐Ÿ›  | โ“ | built-in `classnames` | -| Transition | ๐Ÿ‘€ | ๐Ÿ›  | High-level control over suspense | -| Animation | ๐Ÿ‘€ | โœ… | Spring-style animations | -| Native Mobile | ๐Ÿ‘€ | โœ… | Render with cacao | -| Native Desktop | ๐Ÿ‘€ | โœ… | Render with native desktop | -| 3D Renderer | ๐Ÿ‘€ | โœ… | react-three-fiber | - -### Phase 3: Additional Complexity - -| Feature | Dioxus | React | Notes for Dioxus | -| -------------------- | ------ | ----- | ------------------------------------ | -| Portal | โ“ | โœ… | cast elements through tree | -| Error/Panic boundary | ๐Ÿ‘€ | โœ… | catch panics and display custom BSOD | -| Code-splitting | ๐Ÿ‘€ | โœ… | Make bundle smaller/lazy | -| LiveView | ๐Ÿ‘€ | โ“ | Example for SSR + WASM apps | - - โœ… = implemented and working - ๐Ÿ›  = actively being worked on - ๐Ÿ‘€ = not yet implemented or being worked on - โ“ = not sure if will or can implement + +## FAQ: + +### Aren't VDOMs just pure overhead? Why not something like Solid or Svelte? +Remember: Dioxus is a library - not a compiler like Svelte. Plus, the inner VirtualDOM allows Dioxus to easily port into different runtimes, support SSR, and run remotely in the cloud. VDOMs tend to more ergonomic to work with and feel roughly like natural Rust code. The overhead of Dioxus is **extraordinarily** minimal... sure, there may be some overhead but on an order of magnitude lower than the time required to actually update the page. + + +### Isn't the overhead for interacting with the DOM from WASM too much? +The overhead layer between WASM and JS APIs is extremely poorly understood. Rust web benchmarks typically suffer from differences in how Rust and JS cache strings. In Dioxus, we solve most of these issues and our JS Framework Benchmark actually beats the WASM Bindgen benchmark in many cases. Compared to a "pure vanilla JS" solution, Dioxus adds less than 5% of overhead and takes advantage of batched DOM manipulation. + +### Aren't WASM binaries too huge to deploy in production? +WASM binary sizes are another poorly understood characteristic of Rust web apps. 50kb of WASM and 50kb of JS are _not_ made equally. In JS, the code must be downloaded _first_ and _then_ JIT-ted. Just-in-time compiling 50kb of JavaScript takes a while which is why 50kb of JavaScript sounds like a lot! However, with WASM, the code is downloaded and JIT-ted _simultaneously_ through the magic of streaming compilation. By the time the 50kb of Rust is finished downloading, it is already ready to go. Again, Dioxus beats out many benchmarks with time-to-interactivity. + +For reference, Dioxus `hello-world` clocks in at around 70kb gzip or 60kb brotli, and Dioxus supports SSR. + +### Why hooks? Why not MVC, classes, traits, messages, etc? +There are plenty Rust Elm-like frameworks in the world - we were not interested in making another! Instead, we borrowed hooks from React. JS and Rust share many structural similarities, so if you're comfortable with React, then you'll be plenty comfortable with Dioxus. + +### Why a custom DSL? Why not just pure function calls? +The `RSX` DSL is _barely_ a DSL. Rustaceans will find the DSL very similar to simply assembling nested structs, but without the syntactical overhead of "Default" everywhere or having to jump through hoops with the builder pattern. Between RSX, HTML, the Raw Factory API, and the NodeBuilder syntax, there's plenty of options to choose from. + +### What are the build times like? Why on earth would I choose Rust instead of JS/TS/Elm? +Dioxus builds as roughly as fast as a complex WebPack-TypeScript site. Compile times will be slower than an equivalent TypeScript site, but not unbearably slow. The WASM compiler backend for Rust is very fast. Iterating on small components is basically instant and larger apps takes a few seconds. In practice, the compiler guarantees of Rust balance out the rebuild times. + +### What about Yew/Seed/Sycamore/Dominator/Dodrio/Percy? +- Yew and Seed use an Elm-like pattern and don't support SSR or any alternate rendering platforms +- Sycamore and Dominator are more like SolidJS/Svelte, requiring no VDOM but has less naturally-Rusty state management +- Percy isn't quite mature yet +- Dodrio is the spiritual predecessor of Dioxus, but is currently an archived research project without the batteries of Dioxus + +### How do the mobile and desktop renderers work? Is it Electron? +Currently, Dioxus uses your device's native WebView library to draw the page. None of your app code is actually running in the WebView thread, so you can access system resources instead of having to go through something like NodeJS. This means your app will use Safari on macOS/iOS, Edge (Chromium) on Windows, and whatever is the default Web Browser for Linux and Android. Because your code is compiled and running natively, performance is not a problem. You will have to use the various "Escape Hatches" to use browser-native APIs (like WebGL) and work around visual differences in how Safari and Chrome render the page. + +In the future, we are interested in using Webrenderer to provide a fully native renderer without having to go through the system WebView library. In practice, Dioxus mobile and desktop are great for CRUD-style apps, but the ergonomic cross-platform APIs (GPS, Camera, etc) are not there yet. + +### Why NOT Dioxus? +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. + + +## License + +This project is licensed under the [MIT license]. + +[MIT license]: https://github.com/tokio-rs/tokio/blob/master/LICENSE + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Pipette by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/docs/main-concepts/02-hello.md b/docs/main-concepts/02-hello.md index ad91849c9..c33e115e7 100644 --- a/docs/main-concepts/02-hello.md +++ b/docs/main-concepts/02-hello.md @@ -78,15 +78,16 @@ Now, let's edit our `main.rs` file: ```rust use diouxs::prelude::*; + fn main() { - dioxus::web::start(App) + dioxus::web::start(App, |c| c).unwrap(); } -fn App(cx: Context<()>) -> DomTree { +static App: FC<()> = |cx, props| { cx.render(rsx! { div { "Hello, world!" } }) -} +}; ``` Let's dissect our example a bit. @@ -97,24 +98,36 @@ This bit of code imports everything from the the `prelude` module. This brings i use diouxs::prelude::*; ``` -This bit of code starts the WASM renderer as a future (JS Promise) and then awaits it. This is very similar to the `ReactDOM.render()` method you use for a React app. We pass in the `App` function as a our app. +This bit of code starts the WASM renderer as a future (JS Promise) and then awaits it. This is very similar to the `ReactDOM.render()` method you use for a React app. We pass in the `App` function as a our app. The `|c| c` closure provides a way to configure the WebSys app, enabling things like hydration, pre-rendering, and other tweaks. The simplest configuration is just the defaults. + +Calling "start" will launch Dioxus on the web's main thread, running `wasm_bindgen_futures::block_on`. It's possible to "launch" the WebSys renderer as an async function - check the documentation for more details. ```rust fn main() { - dioxus::web::start(App) + dioxus::web::start(App, |c| c).unwrap(); } ``` -Finally, our app. Every component in Dioxus is a function that takes in a `Context` object and returns a `VNode`. +Finally, our app. Every component in Dioxus is a function that takes in `Context` and `Props` and returns an `Option`. ```rust -fn App(cx: Context<()>) -> DomTree { +static App: FC<()> = |cx, props| { cx.render(rsx! { div { "Hello, world!" } }) }; ``` +The closure `FC<()>` syntax is identical to the function syntax, but with lifetimes managed for you. In cases where props need to borrow from their parent, you will need to specify lifetimes using the function syntax: + +```rust +fn App<'a>(cx: Context<'a>, props: &()) -> DomTree<'a> { + cx.render(rsx! { + div { "Hello, world!" } + }) +} +``` + In React, you'll save 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 `Context` object to control how the component renders and stores data. Next, we're greeted with the `rsx!` macro. This lets us add a custom DSL for declaratively building the structure of our app. The semantics of this macro are similar to that of JSX and HTML, though with a familiar Rust-y interface. The `html!` macro is also available for writing components with a JSX/HTML syntax. @@ -135,7 +148,7 @@ If we wanted to golf a bit, we can shrink our hello-world even smaller: ```rust fn main() { - dioxus::web::start(|cx| cx.render(diouxs::rsx!( div { "Hello, World!" } )) + dioxus::web::start(|cx, _| cx.render(diouxs::rsx!( div { "Hello, World!" } )) } ``` diff --git a/docs/main-concepts/03-rsx.md b/docs/main-concepts/03-rsx.md index e4f377d98..c3b51baaa 100644 --- a/docs/main-concepts/03-rsx.md +++ b/docs/main-concepts/03-rsx.md @@ -12,15 +12,14 @@ You'll want to write RSX where you can, and in a future release we'll have a too ```rust #[derive(PartialEq, Props)] -struct ExampleProps { name: &str, pending: bool, count: i32 } +struct ExampleProps { name: &'static str, pending: bool, count: i32 } -fn Example(cx: Context ) -> DomTree { - let ExampleProps { name, pending, count } = cx.props; +static Example: FC = |cx, props| { cx.render(html! {
-

"Hello, {name}!"

-

"Status: {pending}!"

-

"Count {count}!"

+

"Hello, {props.name}!"

+

"Status: {props.pending}!"

+

"Count {props.count}!"

}) } @@ -35,13 +34,12 @@ The Dioxus VSCode extension will eventually provide a macro to convert a selecti It's also a bit easier on the eyes ๐Ÿ™‚ than HTML. ```rust -fn Example(cx: Context) -> DomTree { +static Example: FC = |cx, props| { cx.render(rsx! { div { - // cx derefs to props so you can access fields directly - p {"Hello, {cx.name}!"} - p {"Status: {cx.pending}!"} - p {"Count {cx.count}!"} + p {"Hello, {props.name}!"} + p {"Status: {props.pending}!"} + p {"Count {props.count}!"} } }) } @@ -60,7 +58,7 @@ Commas are entirely optional, but might be useful to delineate between elements The `render` function provides an **extremely efficient** allocator for VNodes and text, so try not to use the `format!` macro in your components. Rust's default `ToString` methods pass through the global allocator, but all text in components is allocated inside a manually-managed Bump arena. To push you in the right direction, all text-based attributes take `std::fmt::Arguments` directly, so you'll want to reach for `format_args!` when the built-in `f-string` interpolation just doesn't cut it. ```rust -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ let text = "example"; @@ -113,9 +111,9 @@ pub static Example: FC<()> = |cx| { // rsx! is lazy, and the underlying closures cannot have the same type // Rendering produces the VNode type {match rand::gen_range::(1..3) { - 1 => rsx!(in cx, h1 { "big" }) - 2 => rsx!(in cx, h2 { "medium" }) - _ => rsx!(in cx, h3 { "small" }) + 1 => rsx!(cx, h1 { "big" }) + 2 => rsx!(cx, h2 { "medium" }) + _ => rsx!(cx, h3 { "small" }) }} // Optionals @@ -128,7 +126,7 @@ pub static Example: FC<()> = |cx| { // Duplicating nodes // Clones the nodes by reference, so they are literally identical {{ - let node = rsx!(in cx, h1{ "TopNode" }); + let node = rsx!(cx, h1{ "TopNode" }); (0..10).map(|_| node.clone()) }} diff --git a/docs/main-concepts/04-hooks.md b/docs/main-concepts/04-hooks.md index c8741a9fd..7979ccc6e 100644 --- a/docs/main-concepts/04-hooks.md +++ b/docs/main-concepts/04-hooks.md @@ -1,5 +1,5 @@ ```rust -fn Example(cx: &mut Context<()>) -> DomTree { +fn Example<'a>(cx: Context<'a>, props: &()) -> DomTree<'a> { let service = use_combubulator(cx); let Status { name, pending, count } = service.info(); html! { @@ -7,7 +7,7 @@ fn Example(cx: &mut Context<()>) -> DomTree {

"Hello, {name}!"

"Status: {pending}!"

"Count {count}!"

-
diff --git a/docs/main-concepts/10-concurrent-mode.md b/docs/main-concepts/10-concurrent-mode.md index c14d66c34..46b3f8e52 100644 --- a/docs/main-concepts/10-concurrent-mode.md +++ b/docs/main-concepts/10-concurrent-mode.md @@ -50,8 +50,8 @@ async fn ExampleLoader(cx: Context<()>) -> Vnode { This API stores the result on the Context object, so the loaded data is taken as reference. */ let name: &Result = use_fetch_data("http://example.com/json", ()) - .place_holder(|cx| rsx!{
"loading..."
}) - .delayed_place_holder(1000, |cx| rsx!{
"still loading..."
}) + .place_holder(|cx, props|rsx!{
"loading..."
}) + .delayed_place_holder(1000, |cx, props|rsx!{
"still loading..."
}) .await; match name { diff --git a/docs/main-concepts/11-arena-memo.md b/docs/main-concepts/11-arena-memo.md index 4d6417a8b..01b01c1e1 100644 --- a/docs/main-concepts/11-arena-memo.md +++ b/docs/main-concepts/11-arena-memo.md @@ -21,9 +21,9 @@ fn test() -> DomTree { } } -static TestComponent: FC<()> = |cx| html!{
"Hello world"
}; +static TestComponent: FC<()> = |cx, props|html!{
"Hello world"
}; -static TestComponent: FC<()> = |cx| { +static TestComponent: FC<()> = |cx, props|{ let g = "BLAH"; html! {
"Hello world"
@@ -31,7 +31,7 @@ static TestComponent: FC<()> = |cx| { }; #[functional_component] -static TestComponent: FC<{ name: String }> = |cx| html! {
"Hello {name}"
}; +static TestComponent: FC<{ name: String }> = |cx, props|html! {
"Hello {name}"
}; ``` ## Why this behavior? diff --git a/docs/main-concepts/12-signals.md b/docs/main-concepts/12-signals.md index aca4675c2..5000faebc 100644 --- a/docs/main-concepts/12-signals.md +++ b/docs/main-concepts/12-signals.md @@ -96,19 +96,19 @@ Sometimes you want a signal to propagate across your app, either through far-awa ```rust const TITLE: Atom = || "".to_string(); -const Provider: FC<()> = |cx| { +const Provider: FC<()> = |cx, props|{ let title = use_signal(&cx, &TITLE); - rsx!(in cx, input { value: title }) + rsx!(cx, input { value: title }) }; ``` If we use the `TITLE` atom in another component, we can cause updates to flow between components without calling render or diffing either component trees: ```rust -const Receiver: FC<()> = |cx| { +const Receiver: FC<()> = |cx, props|{ let title = use_signal(&cx, &TITLE); log::info!("This will only be called once!"); - rsx!(in cx, + rsx!(cx, div { h1 { "{title}" } div {} @@ -132,7 +132,7 @@ Dioxus automatically understands how to use your signals when mixed with iterato ```rust const DICT: AtomFamily = |_| {}; -const List: FC<()> = |cx| { +const List: FC<()> = |cx, props|{ let dict = use_signal(&cx, &DICT); cx.render(rsx!( ul { diff --git a/examples/_examples/basic.rs b/examples/_examples/basic.rs index e70d3de5a..d1f943ec5 100644 --- a/examples/_examples/basic.rs +++ b/examples/_examples/basic.rs @@ -15,7 +15,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (state, set_state) = use_state_classic(cx, || 0); cx.render(rsx! { div { diff --git a/examples/_examples/calculator.rs b/examples/_examples/calculator.rs index 0ba9ad7f8..fee49b504 100644 --- a/examples/_examples/calculator.rs +++ b/examples/_examples/calculator.rs @@ -27,7 +27,7 @@ enum Operator { Div, } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (cur_val, set_cur_val) = use_state_classic(cx, || 0.0_f64); let (operator, set_operator) = use_state_classic(cx, || None as Option); let (display_value, set_display_value) = use_state_classic(cx, || "0".to_string()); diff --git a/examples/_examples/children.rs b/examples/_examples/children.rs index 17d63df35..c30d1fd80 100644 --- a/examples/_examples/children.rs +++ b/examples/_examples/children.rs @@ -13,7 +13,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { cx.render(rsx! { Calcier { h2 {"abc 1"} @@ -25,7 +25,7 @@ static App: FC<()> = |cx| { }) }; -static Calcier: FC<()> = |cx| { +static Calcier: FC<()> = |cx, props| { cx.render(rsx! { div { h1 { diff --git a/examples/_examples/context.rs b/examples/_examples/context.rs index dbed8057a..0658aa711 100644 --- a/examples/_examples/context.rs +++ b/examples/_examples/context.rs @@ -25,7 +25,7 @@ fn main() { #[derive(Debug)] struct CustomContext([&'static str; 3]); -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ cx.use_create_context(|| CustomContext(["Jack", "Jill", "Bob"])); cx.render(rsx! { diff --git a/examples/_examples/fragment.rs b/examples/_examples/fragment.rs index 059f57431..958fa102c 100644 --- a/examples/_examples/fragment.rs +++ b/examples/_examples/fragment.rs @@ -13,7 +13,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { cx.render(rsx! { h2 { "abc 1" } div { diff --git a/examples/_examples/hello.rs b/examples/_examples/hello.rs index ac13aa19f..72b89a3f4 100644 --- a/examples/_examples/hello.rs +++ b/examples/_examples/hello.rs @@ -13,7 +13,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let nodes = (0..500).map(|f| rsx! (li {"div"})); cx.render(rsx! { div { diff --git a/examples/_examples/infer.rs b/examples/_examples/infer.rs index 08de6deed..8d61a3345 100644 --- a/examples/_examples/infer.rs +++ b/examples/_examples/infer.rs @@ -14,7 +14,7 @@ fn main() { } // this is a component -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ let (event, set_event) = use_state_classic(cx, || None); let handler = move |evt| { @@ -50,7 +50,7 @@ struct ExampleProps { name: String, } -pub static Example2: FC = |cx| { +pub static Example2: FC = |cx, props|{ cx.render(rsx! { div { h1 {"hello {cx.name}"} diff --git a/examples/_examples/input.rs b/examples/_examples/input.rs index 83423c8cb..66a0fab7a 100644 --- a/examples/_examples/input.rs +++ b/examples/_examples/input.rs @@ -12,7 +12,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props|{ let (val, set_val) = use_state_classic(cx, || "asd".to_string()); cx.render(rsx! { @@ -37,7 +37,7 @@ static App: FC<()> = |cx| { }) }; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ cx.render(rsx! { div { class: "max-w-lg max-w-xs bg-blue-800 shadow-2xl rounded-lg mx-auto text-center py-12 mt-4 rounded-xl" div { class: "container py-5 max-w-md mx-auto" @@ -50,7 +50,7 @@ pub static Example: FC<()> = |cx| { }) }; -static UserInput: FC<()> = |cx| { +static UserInput: FC<()> = |cx, props|{ let (val, set_val) = use_state_classic(cx, || "asd".to_string()); rsx! { in cx, diff --git a/examples/_examples/jackjill.rs b/examples/_examples/jackjill.rs index 932d17fdb..cb4e95c78 100644 --- a/examples/_examples/jackjill.rs +++ b/examples/_examples/jackjill.rs @@ -9,7 +9,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)) } -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let (name, set_name) = use_state_classic(cx, || "...?"); log::debug!("Running component...."); diff --git a/examples/_examples/jackjillrsx.rs b/examples/_examples/jackjillrsx.rs index fa84a03dc..fcf7c790f 100644 --- a/examples/_examples/jackjillrsx.rs +++ b/examples/_examples/jackjillrsx.rs @@ -9,7 +9,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)) } -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let (name, set_name) = use_state_classic(cx, || "...?"); cx.render(rsx! { section { class: "py-12 px-4 text-center" diff --git a/examples/_examples/landingpage.rs b/examples/_examples/landingpage.rs index a6589d7ab..2cd24f7c7 100644 --- a/examples/_examples/landingpage.rs +++ b/examples/_examples/landingpage.rs @@ -14,7 +14,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (contents, set_contents) = use_state_classic(cx, || "asd"); cx.render(rsx! { diff --git a/examples/_examples/landingpage2.rs b/examples/_examples/landingpage2.rs index bf47c9f97..53ba2acdd 100644 --- a/examples/_examples/landingpage2.rs +++ b/examples/_examples/landingpage2.rs @@ -13,7 +13,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (contents, set_contents) = use_state_classic(cx, || "asd"); cx.render(rsx! { diff --git a/examples/_examples/list.rs b/examples/_examples/list.rs index f8415902a..4232194b5 100644 --- a/examples/_examples/list.rs +++ b/examples/_examples/list.rs @@ -26,7 +26,7 @@ pub struct TodoItem { pub contents: String, } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (draft, set_draft) = use_state_classic(cx, || "".to_string()); let (filter, set_filter) = use_state_classic(cx, || FilterState::All); let todos = use_state(cx, || BTreeMap::::new()); diff --git a/examples/_examples/many.rs b/examples/_examples/many.rs index be4973acd..cd2027b59 100644 --- a/examples/_examples/many.rs +++ b/examples/_examples/many.rs @@ -11,7 +11,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { span { diff --git a/examples/_examples/model.rs b/examples/_examples/model.rs index 0b70003dd..41906ce92 100644 --- a/examples/_examples/model.rs +++ b/examples/_examples/model.rs @@ -31,7 +31,7 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let state = use_model(&cx, || Calculator::new()); let clear_display = state.display_value.eq("0"); diff --git a/examples/_examples/rsxt.rs b/examples/_examples/rsxt.rs index 6fb139278..f800b7e58 100644 --- a/examples/_examples/rsxt.rs +++ b/examples/_examples/rsxt.rs @@ -27,7 +27,7 @@ struct ExampleProps { initial_name: &'static str, } -pub static Example: FC = |cx| { +pub static Example: FC = |cx, props|{ let name = use_state(cx, move || cx.initial_name); cx.render(rsx! { diff --git a/examples/_examples/todomvc/main.rs b/examples/_examples/todomvc/main.rs index 7b637fcc8..56bd3789d 100644 --- a/examples/_examples/todomvc/main.rs +++ b/examples/_examples/todomvc/main.rs @@ -11,7 +11,7 @@ use dioxus_web::{prelude::*, WebsysRenderer}; static APP_STYLE: &'static str = include_str!("./style.css"); fn main() { - wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|cx| { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|cx, props| { cx.render(rsx! { div { id: "app" diff --git a/examples/_examples/weather.rs b/examples/_examples/weather.rs index f7c25c822..9a2d0b4fc 100644 --- a/examples/_examples/weather.rs +++ b/examples/_examples/weather.rs @@ -9,7 +9,7 @@ fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); console_error_panic_hook::set_once(); - wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|cx| { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|cx, props| { cx.render(html! {
diff --git a/examples/async.rs b/examples/async.rs index 111102921..da7649776 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -1,41 +1,43 @@ -//! Example: README.md showcase -//! -//! The example from the README.md +/* +This example shows how to use async and loops to implement a coroutine in a component. Coroutines can be controlled via +the `TaskHandle` object. +*/ use dioxus::prelude::*; +use gloo_timers::future::TimeoutFuture; + fn main() { - dioxus::desktop::launch(App, |c| c).expect("faield to launch"); + dioxus::desktop::launch(App, |c| c).unwrap(); } -pub static App: FC<()> = |cx| { +pub static App: FC<()> = |cx, _| { let count = use_state(cx, || 0); let mut direction = use_state(cx, || 1); let (async_count, dir) = (count.for_async(), *direction); - let (task, _result) = use_task(cx, move || async move { + + let (task, _) = use_task(cx, move || async move { loop { - gloo_timers::future::TimeoutFuture::new(250).await; + TimeoutFuture::new(250).await; *async_count.get_mut() += dir; } }); - cx.render(rsx! { - div { - h1 {"count is {count}"} - button { - "Stop counting" - onclick: move |_| task.stop() - } - button { - "Start counting" - onclick: move |_| task.resume() - } - button { - "Switch counting direcion" - onclick: move |_| { - direction *= -1; - task.restart(); - } + rsx!(cx, div { + h1 {"count is {count}"} + button { + "Stop counting" + onclick: move |_| task.stop() + } + button { + "Start counting" + onclick: move |_| task.resume() + } + button { + "Switch counting direcion" + onclick: move |_| { + direction *= -1; + task.restart(); } } }) diff --git a/examples/basic.rs b/examples/basic.rs deleted file mode 100644 index 12958072a..000000000 --- a/examples/basic.rs +++ /dev/null @@ -1,14 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - let g = dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - __cx.element( - dioxus_elements::button, - [dioxus::events::on::onclick(__cx, move |_| {})], - [], - [], - None, - ) - }); -} diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 0be0c668b..320d5faa4 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -1,20 +1,18 @@ -#![allow(non_snake_case)] -//! Example: Extremely nested borrowing -//! ----------------------------------- -//! -//! 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. -//! -//! How does it work? -//! -//! Dioxus will manually drop closures and props - things that borrow data before the component is ran again. This is done -//! "bottom up" from the lowest child all the way to the initiating parent. As it traverses each listener and prop, the -//! drop implementation is manually called, freeing any memory and ensuring that memory is not leaked. -//! -//! We cannot drop from the parent to the children - if the drop implementation modifies the data, downstream references -//! might be broken since we take an &mut T and and &T to the data. Instead, we work bottom up, making sure to remove any -//! potential references to the data before finally giving out an &mut T. This prevents us from mutably aliasing the data, -//! and is proven to be safe with MIRI. +/* +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. + +How does it work? + +Dioxus will manually drop closures and props - things that borrow data before the component is ran again. This is done +"bottom up" from the lowest child all the way to the initiating parent. As it traverses each listener and prop, the +drop implementation is manually called, freeing any memory and ensuring that memory is not leaked. + +We cannot drop from the parent to the children - if the drop implementation modifies the data, downstream references +might be broken since we take an &mut T and and &T to the data. Instead, we work bottom up, making sure to remove any +potential references to the data before finally giving out an &mut T. This prevents us from mutably aliasing the data, +and is proven to be safe with MIRI. +*/ use dioxus::prelude::*; @@ -22,7 +20,7 @@ fn main() { dioxus::desktop::launch(App, |c| c); } -fn App<'a>(cx: Context<'a, ()>) -> DomTree<'a> { +fn App<'a>(cx: Context<'a>, props: &()) -> DomTree<'a> { let text: &'a mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); let first = text.get_mut(0).unwrap(); @@ -45,8 +43,8 @@ impl<'a> Drop for C1Props<'a> { fn drop(&mut self) {} } -fn Child1<'a>(cx: Context<'a, C1Props>) -> DomTree<'a> { - let (left, right) = cx.text.split_once("=").unwrap(); +fn Child1<'a>(cx: Context<'a>, props: &'a C1Props) -> DomTree<'a> { + let (left, right) = props.text.split_once("=").unwrap(); cx.render(rsx! { div { @@ -66,10 +64,10 @@ impl<'a> Drop for C2Props<'a> { } } -fn Child2<'a>(cx: Context<'a, C2Props>) -> DomTree<'a> { +fn Child2<'a>(cx: Context<'a>, props: &'a C2Props) -> DomTree<'a> { cx.render(rsx! { Child3 { - text: cx.text + text: props.text } }) } @@ -79,8 +77,8 @@ struct C3Props<'a> { text: &'a str, } -fn Child3<'a>(cx: Context<'a, C3Props>) -> DomTree<'a> { +fn Child3<'a>(cx: Context<'a>, props: &C3Props) -> DomTree<'a> { cx.render(rsx! { - div { "{cx.text}"} + div { "{props.text}"} }) } diff --git a/examples/calculator.rs b/examples/calculator.rs index 2dbbde02b..7108b368b 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -1,44 +1,36 @@ -//! Example: Calculator -//! ------------------------- - -// use dioxus::events::on::*; -// use dioxus::prelude::*; - -fn main() { - env_logger::init(); - dioxus::desktop::launch(App, |cfg| cfg); -} +/* +This example is a simple iOS-style calculator. This particular example can run any platform - Web, Mobile, Desktop. +This calculator version uses React-style state management. All state is held as individual use_states. +*/ use dioxus::events::on::*; use dioxus::prelude::*; -enum Operator { - Add, - Sub, - Mul, - Div, +fn main() { + dioxus::desktop::launch(APP, |cfg| cfg).unwrap(); } -const App: FC<()> = |cx| { +const APP: FC<()> = |cx, _| { let cur_val = use_state(cx, || 0.0_f64); - let operator = use_state(cx, || None as Option); - let display_value = use_state(cx, || "".to_string()); + let operator = use_state(cx, || None as Option<&'static str>); + let display_value = use_state(cx, || String::from("")); let clear_display = display_value == "0"; let clear_text = if clear_display { "C" } else { "AC" }; - let input_digit = move |num: u8| display_value.get_mut().push_str(num.to_string().as_str()); + let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str()); - let input_dot = move || display_value.get_mut().push_str("."); + let input_dot = move || display_value.modify().push_str("."); let perform_operation = move || { if let Some(op) = operator.as_ref() { let rhs = display_value.parse::().unwrap(); - let new_val = match op { - Operator::Add => *cur_val + rhs, - Operator::Sub => *cur_val - rhs, - Operator::Mul => *cur_val * rhs, - Operator::Div => *cur_val / rhs, + let new_val = match *op { + "+" => *cur_val + rhs, + "-" => *cur_val - rhs, + "*" => *cur_val * rhs, + "/" => *cur_val / rhs, + _ => unreachable!(), }; cur_val.set(new_val); display_value.set(new_val.to_string()); @@ -65,51 +57,54 @@ const App: FC<()> = |cx| { }; let keydownhandler = move |evt: KeyboardEvent| match evt.key_code() { + KeyCode::Add => operator.set(Some("+")), + KeyCode::Subtract => operator.set(Some("-")), + KeyCode::Divide => operator.set(Some("/")), + KeyCode::Multiply => operator.set(Some("*")), + KeyCode::Num0 => input_digit(0), + KeyCode::Num1 => input_digit(1), + KeyCode::Num2 => input_digit(2), + KeyCode::Num3 => input_digit(3), + KeyCode::Num4 => input_digit(4), + KeyCode::Num5 => input_digit(5), + KeyCode::Num6 => input_digit(6), + KeyCode::Num7 => input_digit(7), + KeyCode::Num8 => input_digit(8), + KeyCode::Num9 => input_digit(9), KeyCode::Backspace => { if !display_value.as_str().eq("0") { - display_value.get_mut().pop(); + display_value.modify().pop(); } } - KeyCode::_0 => input_digit(0), - KeyCode::_1 => input_digit(1), - KeyCode::_2 => input_digit(2), - KeyCode::_3 => input_digit(3), - KeyCode::_4 => input_digit(4), - KeyCode::_5 => input_digit(5), - KeyCode::_6 => input_digit(6), - KeyCode::_7 => input_digit(7), - KeyCode::_8 => input_digit(8), - KeyCode::_9 => input_digit(9), - KeyCode::Add => operator.set(Some(Operator::Add)), - KeyCode::Subtract => operator.set(Some(Operator::Sub)), - KeyCode::Divide => operator.set(Some(Operator::Div)), - KeyCode::Multiply => operator.set(Some(Operator::Mul)), _ => {} }; - cx.render(rsx! { - div { class: "calculator", onkeydown: {keydownhandler} - div { class: "input-keys" - div { class: "function-keys" - CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" } - CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "ยฑ"} - CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"} - } - div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "โ—" } + use separator::Separatable; + let formatted_display = cur_val.separated_string(); - {(1..9).map(move |k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } - })} - } - div { class: "operator-keys" - CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "รท" } - CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "ร—" } - CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "โˆ’" } - CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" } - CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" } - } + rsx!(cx, div { + class: "calculator", onkeydown: {keydownhandler} + div { class: "calculator-display", "{formatted_display}" } + div { class: "input-keys" + div { class: "function-keys" + CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "ยฑ"} + CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"} + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "โ—" } + + {(1..9).map(|k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } + })} + } + div { class: "operator-keys" + CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some("/")) "รท" } + CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "ร—" } + CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "โˆ’" } + CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" } + CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" } } } }) @@ -117,36 +112,14 @@ const App: FC<()> = |cx| { #[derive(Props)] struct CalculatorKeyProps<'a> { - /// Name! name: &'static str, - - /// Click! onclick: &'a dyn Fn(MouseEvent), } -fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> DomTree<'a> { - cx.render(rsx! { - button { - class: "calculator-key {cx.name}" - onclick: {cx.onclick} - {cx.children()} - } - }) -} - -#[derive(Props, PartialEq)] -struct CalculatorDisplayProps { - val: f64, -} - -fn CalculatorDisplay(cx: Context) -> DomTree { - use separator::Separatable; - // Todo, add float support to the num-format crate - let formatted = cx.val.separated_string(); - // TODO: make it autoscaling with css - cx.render(rsx! { - div { class: "calculator-display" - "{formatted}" - } +fn CalculatorKey<'a>(cx: Context<'a>, props: &'a CalculatorKeyProps) -> DomTree<'a> { + rsx!(cx, button { + class: "calculator-key {props.name}" + onclick: {props.onclick} + {cx.children()} }) } diff --git a/examples/coroutine.rs b/examples/coroutine.rs index 26386055c..ef2dfc9b9 100644 --- a/examples/coroutine.rs +++ b/examples/coroutine.rs @@ -27,7 +27,7 @@ fn main() { use dioxus::prelude::*; -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let p1 = use_state(cx, || 0); let p2 = use_state(cx, || 0); @@ -59,7 +59,7 @@ struct HorseyProps { pos: i32, } -static Horsey: FC = |cx| { +static Horsey: FC = |cx, props| { cx.render(rsx! { div { button { "pause" } diff --git a/examples/crm.rs b/examples/crm.rs new file mode 100644 index 000000000..5155221c3 --- /dev/null +++ b/examples/crm.rs @@ -0,0 +1,104 @@ +/* +Tiny CRM: A port of the Yew CRM example to Dioxus. +*/ +use dioxus::prelude::*; + +fn main() { + dioxus::web::launch(App, |c| c); +} +enum Scene { + ClientsList, + NewClientForm, + Settings, +} + +#[derive(Clone, Debug, Default)] +pub struct Client { + pub first_name: String, + pub last_name: String, + pub description: String, +} + +static App: FC<()> = |cx, _| { + 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 = match *scene { + Scene::ClientsList => { + rsx!(cx, div { class: "crm" + h2 { "List of clients" margin_bottom: "10px" } + div { class: "clients" margin_left: "10px" + {clients.read().iter().map(|client| rsx!( + div { class: "client" style: "margin-bottom: 50px" + p { "First Name: {client.first_name}" } + p { "Last Name: {client.last_name}" } + p {"Description: {client.description}"} + }) + )} + } + 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" } + }) + } + Scene::NewClientForm => { + let add_new = move |_| { + clients.write().push(Client { + description: (*description).clone(), + first_name: (*firstname).clone(), + last_name: (*lastname).clone(), + }); + description.set(String::new()); + firstname.set(String::new()); + lastname.set(String::new()); + }; + rsx!(cx, div { class: "crm" + h2 {"Add new client" margin_bottom: "10px" } + form { class: "pure-form" + input { class: "new-client firstname" placeholder: "First name" value: "{firstname}" + oninput: move |e| firstname.set(e.value()) + } + input { class: "new-client lastname" placeholder: "Last name" value: "{lastname}" + oninput: move |e| lastname.set(e.value()) + } + textarea { class: "new-client description" placeholder: "Description" value: "{description}" + oninput: move |e| description.set(e.value()) + } + } + button { class: "pure-button pure-button-primary", onclick: {add_new}, "Add New" } + button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), "Go Back" } + }) + } + Scene::Settings => { + rsx!(cx, div { + h2 {"Settings" margin_bottom: "10px" } + button { + background: "rgb(202, 60, 60)" + class: "pure-button pure-button-primary" + onclick: move |_| clients.write().clear(), + "Remove all clients" + } + button { + class: "pure-button pure-button-primary" + onclick: move |_| scene.set(Scene::ClientsList), + "Go Back" + } + }) + } + }; + + rsx!(cx, body { + link { + rel: "stylesheet" + href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" + integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" + crossorigin: "anonymous" + } + margin_left: "35%" + h1 {"Dioxus CRM Example"} + {scene} + }) +}; diff --git a/examples/examples/calculator.rs b/examples/examples/calculator.rs index ebebf218b..107794164 100644 --- a/examples/examples/calculator.rs +++ b/examples/examples/calculator.rs @@ -19,7 +19,7 @@ fn main() { .expect("Webview finished"); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let state = use_model(&cx, || Calculator::new()); let clear_display = state.display_value.eq("0"); diff --git a/examples/examples/demo.rs b/examples/examples/demo.rs index 44017d2e0..bcd898055 100644 --- a/examples/examples/demo.rs +++ b/examples/examples/demo.rs @@ -17,7 +17,7 @@ fn main() { .expect("Webview finished"); } -// pub static Example: FC<()> = |cx| { +// pub static Example: FC<()> = |cx, props|{ // cx.render(html! { //
// // }) // }; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { class: "flex items-center justify-center flex-col" diff --git a/examples/examples/hifive.rs b/examples/examples/hifive.rs index 6b44ec7cf..972eff4ff 100644 --- a/examples/examples/hifive.rs +++ b/examples/examples/hifive.rs @@ -17,7 +17,7 @@ fn main() { .expect("Webview finished"); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let hifives = use_model(&cx, || 0); cx.render(rsx! { div { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index bedb3d23d..da543e5c3 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -7,7 +7,6 @@ use dioxus::desktop::wry::application::dpi::LogicalSize; use dioxus::prelude::*; -use std::fs::{self, DirEntry}; fn main() { env_logger::init(); @@ -20,12 +19,12 @@ fn main() { .unwrap(); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let files = use_state(cx, || Files::new()); let file_list = files.path_names.iter().enumerate().map(|(dir_id, path)| { rsx! ( - li { a {"{path}", onclick: move |_| files.get_mut().enter_dir(dir_id), href: "#"} } + li { a {"{path}", onclick: move |_| files.modify().enter_dir(dir_id), href: "#"} } ) }); @@ -33,7 +32,7 @@ static App: FC<()> = |cx| { rsx! { div { code {"{err}"} - button {"x", onclick: move |_| files.get_mut().clear_err() } + button {"x", onclick: move |_| files.modify().clear_err() } } } }); @@ -43,7 +42,7 @@ static App: FC<()> = |cx| { div { h1 {"Files: "} h3 {"Cur dir: {cur}"} - button { "go up", onclick: move |_| files.get_mut().go_up() } + button { "go up", onclick: move |_| files.modify().go_up() } ol { {file_list} } {err_disp} } @@ -73,7 +72,7 @@ impl Files { fn reload_path_list(&mut self) { let cur_path = self.path_stack.last().unwrap(); - let paths = match fs::read_dir(cur_path) { + let paths = match std::fs::read_dir(cur_path) { Ok(e) => e, Err(err) => { let err = format!("An error occured: {:?}", err); diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index a7ee8588d..8927198a9 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -19,7 +19,7 @@ fn main() { // We use a special immutable hashmap to make hashmap operations efficient type RowList = im_rc::HashMap, FxBuildHasher>; -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let items = use_state(cx, || RowList::default()); let create_rendered_rows = move |from, num| move |_| items.set(create_row_list(from, num)); @@ -92,14 +92,14 @@ struct ActionButtonProps { id: &'static str, action: F, } -fn ActionButton(cx: Context>) -> DomTree +fn ActionButton<'a, F>(cx: Context<'a>, props: &'a ActionButtonProps) -> DomTree<'a> where F: Fn(MouseEvent), { cx.render(rsx! { div { class: "col-sm-6 smallpad" - button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.id}", onclick: {&cx.action}, - "{cx.name}" + button { class:"btn btn-primary btn-block", r#type: "button", id: "{props.id}", onclick: {&props.action}, + "{props.name}" } } }) @@ -110,12 +110,12 @@ struct RowProps { row_id: usize, label: Rc, } -fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { +fn Row<'a>(cx: Context<'a>, props: &'a RowProps) -> DomTree<'a> { cx.render(rsx! { tr { - td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", "{props.row_id}" } td { class:"col-md-1", onclick: move |_| { /* run onselect */ } - a { class: "lbl", "{cx.label}" } + a { class: "lbl", "{props.label}" } } td { class: "col-md-1" a { class: "remove", onclick: move |_| {/* remove */} diff --git a/examples/hydration.rs b/examples/hydration.rs index 0f083d933..a661ae842 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -19,7 +19,7 @@ fn main() { dioxus::desktop::launch(App, |c| c.with_prerendered(content)).unwrap(); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let mut val = use_state(cx, || 0); cx.render(rsx! { div { diff --git a/examples/manually.rs b/examples/manually.rs index 4e6999d96..584b91756 100644 --- a/examples/manually.rs +++ b/examples/manually.rs @@ -6,7 +6,6 @@ fn main() { // .. should result in an "invalid node tree" let edits = vec![ CreateElement { tag: "div", id: 0 }, - // CreatePlaceholder { id: 1 }, CreateElement { tag: "h1", id: 2 }, CreateTextNode { text: "hello world", @@ -15,9 +14,9 @@ fn main() { AppendChildren { many: 1 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, - // ReplaceWith { many: 1 }, ]; + dioxus_desktop::WebviewRenderer::run_with_edits(App, (), |c| c, Some(edits)).expect("failed"); } -const App: FC<()> = |cx| todo!(); +const App: FC<()> = |cx, props| todo!(); diff --git a/examples/model.rs b/examples/model.rs index f65355a8a..f7012fa32 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -32,41 +32,39 @@ fn main() { .expect("failed to launch dioxus app"); } -static App: FC<()> = |cx| { - let state = use_state(cx, || Calculator::new()); +static App: FC<()> = |cx, props| { + let state = use_ref(cx, || Calculator::new()); - let clear_display = state.display_value.eq("0"); + let clear_display = state.read().display_value.eq("0"); let clear_text = if clear_display { "C" } else { "AC" }; - let formatted = state.formatted_display(); + let formatted = state.read().formatted_display(); - cx.render(rsx! { - div { id: "wrapper" - div { class: "app", style { "{STYLE}" } - div { class: "calculator", onkeypress: move |evt| state.get_mut().handle_keydown(evt), - div { class: "calculator-display", "{formatted}"} - div { class: "calculator-keypad" - div { class: "input-keys" - div { class: "function-keys" - CalculatorKey { name: "key-clear", onclick: move |_| state.get_mut().clear_display(), "{clear_text}" } - CalculatorKey { name: "key-sign", onclick: move |_| state.get_mut().toggle_sign(), "ยฑ"} - CalculatorKey { name: "key-percent", onclick: move |_| state.get_mut().toggle_percent(), "%"} - } - div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| state.get_mut().input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| state.get_mut().input_dot(), "โ—" } - {(1..10).map(move |k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.get_mut().input_digit(k), "{k}" } - })} - } + rsx!(cx, div { id: "wrapper" + div { class: "app", style { "{STYLE}" } + div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt), + div { class: "calculator-display", "{formatted}"} + div { class: "calculator-keypad" + div { class: "input-keys" + div { class: "function-keys" + CalculatorKey { name: "key-clear", onclick: move |_| state.write().clear_display(), "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: move |_| state.write().toggle_sign(), "ยฑ"} + CalculatorKey { name: "key-percent", onclick: move |_| state.write().toggle_percent(), "%"} } - div { class: "operator-keys" - CalculatorKey { name:"key-divide", onclick: move |_| state.get_mut().set_operator(Operator::Div), "รท" } - CalculatorKey { name:"key-multiply", onclick: move |_| state.get_mut().set_operator(Operator::Mul), "ร—" } - CalculatorKey { name:"key-subtract", onclick: move |_| state.get_mut().set_operator(Operator::Sub), "โˆ’" } - CalculatorKey { name:"key-add", onclick: move |_| state.get_mut().set_operator(Operator::Add), "+" } - CalculatorKey { name:"key-equals", onclick: move |_| state.get_mut().perform_operation(), "=" } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| state.write().input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| state.write().input_dot(), "โ—" } + {(1..10).map(move |k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.write().input_digit(k), "{k}" } + })} } } + div { class: "operator-keys" + CalculatorKey { name:"key-divide", onclick: move |_| state.write().set_operator(Operator::Div), "รท" } + CalculatorKey { name:"key-multiply", onclick: move |_| state.write().set_operator(Operator::Mul), "ร—" } + CalculatorKey { name:"key-subtract", onclick: move |_| state.write().set_operator(Operator::Sub), "โˆ’" } + CalculatorKey { name:"key-add", onclick: move |_| state.write().set_operator(Operator::Add), "+" } + CalculatorKey { name:"key-equals", onclick: move |_| state.write().perform_operation(), "=" } + } } } } @@ -79,17 +77,16 @@ struct CalculatorKeyProps<'a> { onclick: &'a dyn Fn(MouseEvent), } -fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> DomTree<'a> { +fn CalculatorKey<'a, 'r>(cx: Context<'a>, props: &'a CalculatorKeyProps) -> DomTree<'a> { cx.render(rsx! { button { - class: "calculator-key {cx.name}" - onclick: {cx.onclick} + class: "calculator-key {props.name}" + onclick: {props.onclick} {cx.children()} } }) } -#[derive(Clone)] struct Calculator { display_value: String, operator: Option, @@ -174,16 +171,16 @@ impl Calculator { fn handle_keydown(&mut self, evt: KeyboardEvent) { match evt.key_code() { KeyCode::Backspace => self.backspace(), - KeyCode::_0 => self.input_digit(0), - KeyCode::_1 => self.input_digit(1), - KeyCode::_2 => self.input_digit(2), - KeyCode::_3 => self.input_digit(3), - KeyCode::_4 => self.input_digit(4), - KeyCode::_5 => self.input_digit(5), - KeyCode::_6 => self.input_digit(6), - KeyCode::_7 => self.input_digit(7), - KeyCode::_8 => self.input_digit(8), - KeyCode::_9 => self.input_digit(9), + KeyCode::Num0 => self.input_digit(0), + KeyCode::Num1 => self.input_digit(1), + KeyCode::Num2 => self.input_digit(2), + KeyCode::Num3 => self.input_digit(3), + KeyCode::Num4 => self.input_digit(4), + KeyCode::Num5 => self.input_digit(5), + KeyCode::Num6 => self.input_digit(6), + KeyCode::Num7 => self.input_digit(7), + KeyCode::Num8 => self.input_digit(8), + KeyCode::Num9 => self.input_digit(9), KeyCode::Add => self.operator = Some(Operator::Add), KeyCode::Subtract => self.operator = Some(Operator::Sub), KeyCode::Divide => self.operator = Some(Operator::Div), diff --git a/examples/readme.rs b/examples/readme.rs index 82520b163..8eb8b098f 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -7,7 +7,7 @@ fn main() { dioxus::desktop::launch(App, |c| c); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); cx.render(rsx! { diff --git a/examples/reducer.rs b/examples/reducer.rs index 2f93beeee..a23bd2659 100644 --- a/examples/reducer.rs +++ b/examples/reducer.rs @@ -11,23 +11,21 @@ fn main() { dioxus::desktop::launch(App, |c| c); } -pub static App: FC<()> = |cx| { +pub static App: FC<()> = |cx, _| { let state = use_state(cx, PlayerState::new); let is_playing = state.is_playing(); - cx.render(rsx! { - div { - h1 {"Select an option"} - h3 {"The radio is... {is_playing}!"} - button { - "Pause" - onclick: move |_| state.get_mut().reduce(PlayerAction::Pause) - } - button { - "Play" - onclick: move |_| state.get_mut().reduce(PlayerAction::Play) - } + rsx!(cx, div { + h1 {"Select an option"} + h3 {"The radio is... {is_playing}!"} + button { + "Pause" + onclick: move |_| state.modify().reduce(PlayerAction::Pause) + } + button { + "Play" + onclick: move |_| state.modify().reduce(PlayerAction::Play) } }) }; diff --git a/examples/reference/antipatterns.rs b/examples/reference/antipatterns.rs index 90b694a63..39d70bd9f 100644 --- a/examples/reference/antipatterns.rs +++ b/examples/reference/antipatterns.rs @@ -32,14 +32,14 @@ use dioxus::prelude::*; struct NoKeysProps { data: std::collections::HashMap, } -static AntipatternNoKeys: FC = |cx| { +static AntipatternNoKeys: FC = |cx, props| { // WRONG: Make sure to add keys! - rsx!(in cx, ul { - {cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} + rsx!(cx, ul { + {props.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} }); // RIGHT: Like this: - rsx!(in cx, ul { - {cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))} + rsx!(cx, ul { + {props.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))} }) }; @@ -54,9 +54,9 @@ static AntipatternNoKeys: FC = |cx| { /// /// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing /// an API for registering shared state without the ContextProvider pattern. -static AntipatternNestedFragments: FC<()> = |cx| { +static AntipatternNestedFragments: FC<()> = |cx, props| { // Try to avoid heavily nesting fragments - rsx!(in cx, + rsx!(cx, Fragment { Fragment { Fragment { @@ -82,7 +82,7 @@ static AntipatternNestedFragments: FC<()> = |cx| { /// However, calling set_state will *not* update the current version of state in the component. This should be easy to /// recognize from the function signature, but Dioxus will not update the "live" version of state. Calling `set_state` /// merely places a new value in the queue and schedules the component for a future update. -static AntipaternRelyingOnSetState: FC<()> = |cx| { +static AntipaternRelyingOnSetState: FC<()> = |cx, props| { let (state, set_state) = use_state(cx, || "Hello world").classic(); set_state("New state"); // This will return false! `state` will *still* be "Hello world" @@ -99,7 +99,7 @@ static AntipaternRelyingOnSetState: FC<()> = |cx| { /// - All components must start with an uppercase character /// /// IE: the following component will be rejected when attempted to be used in the rsx! macro -static antipattern_component: FC<()> = |cx| todo!(); +static antipattern_component: FC<()> = |cx, props| todo!(); /// Antipattern: Misusing hooks /// --------------------------- @@ -120,14 +120,14 @@ static antipattern_component: FC<()> = |cx| todo!(); struct MisuedHooksProps { should_render_state: bool, } -static AntipatternMisusedHooks: FC = |cx| { - if cx.should_render_state { +static AntipatternMisusedHooks: FC = |cx, props| { + if props.should_render_state { // do not place a hook in the conditional! // prefer to move it out of the conditional let (state, set_state) = use_state(cx, || "hello world").classic(); - rsx!(in cx, div { "{state}" }) + rsx!(cx, div { "{state}" }) } else { - rsx!(in cx, div { "Not rendering state" }) + rsx!(cx, div { "Not rendering state" }) } }; @@ -153,7 +153,7 @@ static AntipatternMisusedHooks: FC = |cx| { /// } /// } /// }) -static _example: FC<()> = |cx| todo!(); +static _example: FC<()> = |cx, props| todo!(); /// Antipattern: publishing components and hooks with all features enabled /// ---------------------------------------------------------------------- @@ -171,9 +171,9 @@ static _example: FC<()> = |cx| todo!(); /// /// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific /// libraries. -static __example: FC<()> = |cx| todo!(); +static __example: FC<()> = |cx, props| todo!(); -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { AntipatternNoKeys { data: std::collections::HashMap::new() } AntipatternNestedFragments {} diff --git a/examples/reference/basics.rs b/examples/reference/basics.rs index aefc10300..f4c945c7c 100644 --- a/examples/reference/basics.rs +++ b/examples/reference/basics.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { Greeting { @@ -25,10 +25,10 @@ struct GreetingProps { name: &'static str, } -static Greeting: FC = |cx| { +static Greeting: FC = |cx, props| { cx.render(rsx! { div { - h1 { "Hello, {cx.name}!" } + h1 { "Hello, {props.name}!" } p { "Welcome to the Diouxs framework" } br {} {cx.children()} diff --git a/examples/reference/children.rs b/examples/reference/children.rs index dc4e67838..16e89e52e 100644 --- a/examples/reference/children.rs +++ b/examples/reference/children.rs @@ -18,7 +18,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { Banner { @@ -31,7 +31,7 @@ pub static Example: FC<()> = |cx| { }) }; -pub static Banner: FC<()> = |cx| { +pub static Banner: FC<()> = |cx, props| { cx.render(rsx! { div { h1 { "This is a great banner!" } diff --git a/examples/reference/conditional_rendering.rs b/examples/reference/conditional_rendering.rs index 6a349d54e..80be8094a 100644 --- a/examples/reference/conditional_rendering.rs +++ b/examples/reference/conditional_rendering.rs @@ -16,10 +16,10 @@ use dioxus::prelude::*; pub struct MyProps { should_show: bool, } -pub static Example0: FC = |cx| { +pub static Example0: FC = |cx, props| { cx.render(rsx! { div { - {cx.should_show.then(|| rsx!{ + {props.should_show.then(|| rsx!{ h1 { "showing the title!" } })} } @@ -34,32 +34,32 @@ pub static Example0: FC = |cx| { // which will do essentially the same thing as `cx.render`. // // In short: -// `rsx!(in cx, ...)` is shorthand for `cx.render(rsx!(...))` +// `rsx!(cx, ...)` is shorthand for `cx.render(rsx!(...))` #[derive(PartialEq, Props)] pub struct MyProps1 { should_show: bool, } -pub static Example1: FC = |cx| { +pub static Example1: FC = |cx, props| { cx.render(rsx! { div { // With matching - {match cx.should_show { + {match props.should_show { true => cx.render(rsx!(div {"it is true!"})), - false => rsx!(in cx, div {"it is false!"}), + false => rsx!(cx, div {"it is false!"}), }} // or with just regular conditions - {if cx.should_show { - rsx!(in cx, div {"it is true!"}) + {if props.should_show { + rsx!(cx, div {"it is true!"}) } else { - rsx!(in cx, div {"it is false!"}) + rsx!(cx, div {"it is false!"}) }} // or with optional chaining { - cx.should_show - .then(|| rsx!(in cx, div {"it is false!"})) - .unwrap_or_else(|| rsx!(in cx, div {"it is false!"})) + props.should_show + .then(|| rsx!(cx, div {"it is false!"})) + .unwrap_or_else(|| rsx!(cx, div {"it is false!"})) } } }) @@ -77,19 +77,19 @@ pub enum Color { pub struct MyProps2 { color: Color, } -pub static Example2: FC = |cx| { +pub static Example2: FC = |cx, props| { cx.render(rsx! { div { - {match cx.color { - Color::Green => rsx!(in cx, div {"it is Green!"}), - Color::Yellow => rsx!(in cx, div {"it is Yellow!"}), - Color::Red => rsx!(in cx, div {"it is Red!"}), + {match props.color { + Color::Green => rsx!(cx, div {"it is Green!"}), + Color::Yellow => rsx!(cx, div {"it is Yellow!"}), + Color::Red => rsx!(cx, div {"it is Red!"}), }} } }) }; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let should_show = use_state(cx, || false); let mut color_index = use_state(cx, || 0); let color = match *color_index % 2 { diff --git a/examples/reference/controlled_inputs.rs b/examples/reference/controlled_inputs.rs index 16e37e491..bdabe83d2 100644 --- a/examples/reference/controlled_inputs.rs +++ b/examples/reference/controlled_inputs.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { @@ -10,7 +10,7 @@ pub static Example: FC<()> = |cx| { }; // A controlled component: -static ControlledSelect: FC<()> = |cx| { +static ControlledSelect: FC<()> = |cx, props| { let value = use_state(cx, || String::from("Grapefruit")); cx.render(rsx! { select { value: "{value}", onchange: move |evt| value.set(evt.value()), @@ -23,7 +23,7 @@ static ControlledSelect: FC<()> = |cx| { }; // TODO - how do uncontrolled things work? -static UncontrolledSelect: FC<()> = |cx| { +static UncontrolledSelect: FC<()> = |cx, props| { let value = use_state(cx, || String::new()); cx.render(rsx! { diff --git a/examples/reference/custom_elements.rs b/examples/reference/custom_elements.rs index 7b86430d1..053b3aea5 100644 --- a/examples/reference/custom_elements.rs +++ b/examples/reference/custom_elements.rs @@ -11,7 +11,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { custom_element { diff --git a/examples/reference/empty.rs b/examples/reference/empty.rs index d852b5bb2..28914bd6b 100644 --- a/examples/reference/empty.rs +++ b/examples/reference/empty.rs @@ -5,4 +5,4 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| cx.render(rsx! { Fragment {} }); +pub static Example: FC<()> = |cx, props| cx.render(rsx! { Fragment {} }); diff --git a/examples/reference/errorhandling.rs b/examples/reference/errorhandling.rs index ba0f21a87..a79153272 100644 --- a/examples/reference/errorhandling.rs +++ b/examples/reference/errorhandling.rs @@ -23,14 +23,14 @@ fn main() {} /// This is one way to go about error handling (just toss things away with unwrap). /// However, if you get it wrong, the whole app will crash. /// This is pretty flimsy. -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let data = get_data().unwrap(); cx.render(rsx!( div { "{data}" } )) }; /// This is a pretty verbose way of error handling /// However, it's still pretty good since we don't panic, just fail to render anything -static App1: FC<()> = |cx| { +static App1: FC<()> = |cx, props| { let data = match get_data() { Some(data) => data, None => return None, @@ -46,7 +46,7 @@ static App1: FC<()> = |cx| { /// a user is logged in. /// /// Dioxus will throw an error in the console if the None-path is ever taken. -static App2: FC<()> = |cx| { +static App2: FC<()> = |cx, props| { let data = get_data()?; cx.render(rsx!( div { "{data}" } )) }; @@ -54,14 +54,14 @@ static App2: FC<()> = |cx| { /// This is top-tier error handling since it displays a failure state. /// /// However, the error is lacking in context. -static App3: FC<()> = |cx| match get_data() { +static App3: FC<()> = |cx, props| match get_data() { Some(data) => cx.render(rsx!( div { "{data}" } )), None => cx.render(rsx!( div { "Failed to load data :(" } )), }; /// For errors that return results, it's possible short-circuit the match-based error handling with `.ok()` which converts /// a Result into an Option and lets you -static App4: FC<()> = |cx| { +static App4: FC<()> = |cx, props| { let data = get_data_err().ok()?; cx.render(rsx!( div { "{data}" } )) }; @@ -69,7 +69,7 @@ static App4: FC<()> = |cx| { /// This is great error handling since it displays a failure state... with context! /// /// Hopefully you never need to disply a screen like this. It's rather bad taste -static App5: FC<()> = |cx| match get_data_err() { +static App5: FC<()> = |cx, props| match get_data_err() { Ok(data) => cx.render(rsx!( div { "{data}" } )), Err(c) => cx.render(rsx!( div { "Failed to load data: {c}" } )), }; diff --git a/examples/reference/fragments.rs b/examples/reference/fragments.rs index 7ebde4229..88c9dcaea 100644 --- a/examples/reference/fragments.rs +++ b/examples/reference/fragments.rs @@ -11,7 +11,7 @@ use dioxus::prelude::*; // Returning multiple elements with rsx! or html! -static App1: FC<()> = |cx| { +static App1: FC<()> = |cx, props| { cx.render(rsx! { h1 { } h2 { } @@ -20,7 +20,7 @@ static App1: FC<()> = |cx| { }; // Using the Fragment component -static App2: FC<()> = |cx| { +static App2: FC<()> = |cx, props| { cx.render(rsx! { Fragment { div {} @@ -31,7 +31,7 @@ static App2: FC<()> = |cx| { }; // Using the `fragment` method on the NodeFactory -static App3: FC<()> = |cx| { +static App3: FC<()> = |cx, props| { cx.render(LazyNodes::new(move |fac| { fac.fragment_from_iter([ fac.text(format_args!("A")), @@ -42,7 +42,7 @@ static App3: FC<()> = |cx| { })) }; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { App1 {} App2 {} diff --git a/examples/reference/global_css.rs b/examples/reference/global_css.rs index eb3cb73be..5e7c75a8c 100644 --- a/examples/reference/global_css.rs +++ b/examples/reference/global_css.rs @@ -19,7 +19,7 @@ h1 {color: blue;} p {color: red;} "#; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { head { style { "{STYLE}" } } body { diff --git a/examples/reference/inline_styles.rs b/examples/reference/inline_styles.rs index f75e2ac13..272e220c2 100644 --- a/examples/reference/inline_styles.rs +++ b/examples/reference/inline_styles.rs @@ -10,7 +10,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { head { style: { background_color: "powderblue" } @@ -29,7 +29,7 @@ pub static Example: FC<()> = |cx| { // .... technically the rsx! macro is slightly broken at the moment and alows styles not wrapped in style {} // I haven't noticed any name collisions yet, and am tentatively leaving this behavior in.. // Don't rely on it. -static Example2: FC<()> = |cx| { +static Example2: FC<()> = |cx, props| { cx.render(rsx! { div { color: "red" "hello world!" diff --git a/examples/reference/iterators.rs b/examples/reference/iterators.rs index 9067bb46c..ed4fdfd13 100644 --- a/examples/reference/iterators.rs +++ b/examples/reference/iterators.rs @@ -12,7 +12,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let example_data = use_state(cx, || 0); let v = (0..10).map(|f| { diff --git a/examples/reference/listener.rs b/examples/reference/listener.rs index 454b0087e..59d180263 100644 --- a/examples/reference/listener.rs +++ b/examples/reference/listener.rs @@ -7,7 +7,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { ButtonList {} NonUpdatingEvents {} @@ -16,7 +16,7 @@ pub static Example: FC<()> = |cx| { }; /// We can use `set_name` in multiple closures; the closures automatically *copy* the reference to set_name. -static ButtonList: FC<()> = |cx| { +static ButtonList: FC<()> = |cx, props| { let name = use_state(cx, || "...?"); let names = ["jack", "jill", "john", "jane"] @@ -33,8 +33,8 @@ static ButtonList: FC<()> = |cx| { /// This shows how listeners may be without a visible change in the display. /// Check the console. -static NonUpdatingEvents: FC<()> = |cx| { - rsx!(in cx, div { +static NonUpdatingEvents: FC<()> = |cx, props| { + rsx!(cx, div { button { onclick: move |_| log::info!("Did not cause any updates!") "Click me to log!" @@ -42,8 +42,8 @@ static NonUpdatingEvents: FC<()> = |cx| { }) }; -static DisablePropogation: FC<()> = |cx| { - rsx!(in cx, +static DisablePropogation: FC<()> = |cx, props| { + rsx!(cx, div { onclick: move |_| log::info!("event propogated to the div!") button { diff --git a/examples/reference/memo.rs b/examples/reference/memo.rs index b9b1985d5..40aa54191 100644 --- a/examples/reference/memo.rs +++ b/examples/reference/memo.rs @@ -21,7 +21,7 @@ use dioxus::prelude::*; // By default, components with no props are always memoized. // A props of () is considered empty. -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { "100% memoized!" } }) @@ -35,9 +35,9 @@ pub struct MyProps1 { name: String, } -pub static Example1: FC = |cx| { +pub static Example1: FC = |cx, props| { cx.render(rsx! { - div { "100% memoized! {cx.name}" } + div { "100% memoized! {props.name}" } }) }; @@ -49,9 +49,9 @@ pub struct MyProps2 { name: std::rc::Rc, } -pub static Example2: FC = |cx| { +pub static Example2: FC = |cx, props| { cx.render(rsx! { - div { "100% memoized! {cx.name}" } + div { "100% memoized! {props.name}" } }) }; @@ -63,9 +63,9 @@ pub struct MyProps3<'a> { // We need to manually specify a lifetime that ensures props and scope (the component's state) share the same lifetime. // Using the `pub static Example: FC<()>` pattern _will_ specify a lifetime, but that lifetime will be static which might // not exactly be what you want -fn Example3<'a>(cx: Context<'a, MyProps3<'a>>) -> DomTree { +fn Example3<'a>(cx: Context<'a>, props: &'a MyProps3) -> DomTree<'a> { cx.render(rsx! { - div { "Not memoized! {cx.name}" } + div { "Not memoized! {props.name}" } }) } @@ -77,8 +77,8 @@ pub struct MyProps4<'a> { } // We need to manually specify a lifetime that ensures props and scope (the component's state) share the same lifetime. -fn Example4<'a>(cx: Context<'a, MyProps4<'a>>) -> DomTree { +fn Example4<'a>(cx: Context<'a>, props: &'a MyProps4) -> DomTree<'a> { cx.render(rsx! { - div { "Not memoized!", onclick: move |_| (cx.onhandle)() } + div { "Not memoized!", onclick: move |_| (props.onhandle)() } }) } diff --git a/examples/reference/noderefs.rs b/examples/reference/noderefs.rs index 195210cfd..8d22ee5b4 100644 --- a/examples/reference/noderefs.rs +++ b/examples/reference/noderefs.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let p = 10; cx.render(rsx! { diff --git a/examples/reference/signals.rs b/examples/reference/signals.rs index fdab747b6..2e69d8e66 100644 --- a/examples/reference/signals.rs +++ b/examples/reference/signals.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/reference/spreadpattern.rs b/examples/reference/spreadpattern.rs index 4faddf900..770cf427c 100644 --- a/examples/reference/spreadpattern.rs +++ b/examples/reference/spreadpattern.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let props = MyProps { count: 0, live: true, @@ -27,12 +27,12 @@ pub struct MyProps { name: &'static str, } -pub static Example1: FC = |cx| { +pub static Example1: FC = |cx, MyProps { count, live, name }| { cx.render(rsx! { div { - h1 { "Hello, {cx.name}"} - h3 {"Are we alive? {cx.live}"} - p {"Count is {cx.count}"} + h1 { "Hello, {name}"} + h3 {"Are we alive? {live}"} + p {"Count is {count}"} { cx.children() } } }) diff --git a/examples/reference/statemanagement.rs b/examples/reference/statemanagement.rs index fdab747b6..2e69d8e66 100644 --- a/examples/reference/statemanagement.rs +++ b/examples/reference/statemanagement.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/reference/suspense.rs b/examples/reference/suspense.rs index d27572d30..f5431edf6 100644 --- a/examples/reference/suspense.rs +++ b/examples/reference/suspense.rs @@ -14,13 +14,18 @@ struct DogApi { } const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random"; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let doggo = use_suspense( cx, || surf::get(ENDPOINT).recv_json::(), |cx, res| match res { - Ok(res) => rsx!(in cx, img { src: "{res.message}" }), - Err(_) => rsx!(in cx, div { "No doggos for you :(" }), + Ok(res) => rsx!( + cx, + img { + src: "{res.message}" + } + ), + Err(_) => rsx!(cx, div { "No doggos for you :(" }), }, ); diff --git a/examples/reference/task.rs b/examples/reference/task.rs index 8f1e61e0a..a5e877bc6 100644 --- a/examples/reference/task.rs +++ b/examples/reference/task.rs @@ -24,7 +24,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let count = use_state(cx, || 0); let mut direction = use_state(cx, || 1); diff --git a/examples/reference/testing.rs b/examples/reference/testing.rs index d59e9619a..587ce19cc 100644 --- a/examples/reference/testing.rs +++ b/examples/reference/testing.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/reference/tostring.rs b/examples/reference/tostring.rs index 02b1daf81..a04a165da 100644 --- a/examples/reference/tostring.rs +++ b/examples/reference/tostring.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; use dioxus::ssr; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let as_string = use_state(cx, || { // Currently, SSR is only supported for whole VirtualDOMs // This is an easy/low hanging fruit to improve upon @@ -15,7 +15,7 @@ pub static Example: FC<()> = |cx| { }) }; -static SomeApp: FC<()> = |cx| { +static SomeApp: FC<()> = |cx, props| { cx.render(rsx! { div { style: {background_color: "blue"} h1 {"Some amazing app or component"} diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index ba2e9b2f5..0c2569880 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -49,7 +49,7 @@ const NONE_ELEMENT: Option<()> = None; use baller::Baller; use dioxus::prelude::*; -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props| { let formatting = "formatting!"; let formatting_tuple = ("a", "b"); let lazy_fmt = format_args!("lazily formatted text"); @@ -106,7 +106,7 @@ pub static Example: FC<()> = |cx| { // To fix this, call "render" method or use the "in" syntax to produce VNodes. // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) {match true { - true => rsx!(in cx, h1 {"Top text"}), + true => rsx!(cx, h1 {"Top text"}), false => cx.render(rsx!( h1 {"Bottom text"})) }} @@ -117,9 +117,9 @@ pub static Example: FC<()> = |cx| { // True conditions need to be rendered (same reasons as matching) {if true { - rsx!(in cx, h1 {"Top text"}) + rsx!(cx, h1 {"Top text"}) } else { - rsx!(in cx, h1 {"Bottom text"}) + rsx!(cx, h1 {"Bottom text"}) }} // returning "None" is a bit noisy... but rare in practice @@ -180,10 +180,11 @@ pub static Example: FC<()> = |cx| { mod baller { use super::*; + #[derive(PartialEq, Props)] pub struct BallerProps {} /// This component totally balls - pub fn Baller(cx: Context<()>) -> DomTree { + pub fn Baller<'a>(cx: Context<'a>, props: &BallerProps) -> DomTree<'a> { todo!() } } @@ -194,7 +195,7 @@ pub struct TallerProps { } /// This component is taller than most :) -pub fn Taller(cx: Context) -> DomTree { +pub fn Taller<'a>(cx: Context<'a>, props: &'a TallerProps) -> DomTree<'a> { let b = true; todo!() } diff --git a/examples/showcase.rs b/examples/showcase.rs index 12652c887..ca3cc469c 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -11,12 +11,12 @@ fn main() {} use dioxus::prelude::*; use std::rc::Rc; -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let (selection, set_selection) = use_state(cx, || None as Option).classic(); let body = match selection { - Some(id) => rsx!(in cx, ReferenceItem { selected: *id }), - None => rsx!(in cx, div { "Select an concept to explore" }), + Some(id) => rsx!(cx, ReferenceItem { selected: *id }), + None => rsx!(cx, div { "Select an concept to explore" }), }; cx.render(rsx! { @@ -33,7 +33,7 @@ struct ScrollSelectorProps<'a> { onselect: &'a dyn Fn(Option), } -fn ScrollSelector<'a>(cx: Context<'a, ScrollSelectorProps>) -> DomTree<'a> { +fn ScrollSelector<'a>(cx: Context<'a>, props: &'a ScrollSelectorProps) -> DomTree<'a> { let selection_list = (&REFERENCES).iter().enumerate().map(|(id, _)| { rsx! { li { @@ -47,7 +47,7 @@ fn ScrollSelector<'a>(cx: Context<'a, ScrollSelectorProps>) -> DomTree<'a> { ul { {selection_list} button { - onclick: move |_| (cx.onselect)(Some(10)) + onclick: move |_| (props.onselect)(Some(10)) } } } @@ -59,8 +59,8 @@ struct ReferenceItemProps { selected: usize, } -static ReferenceItem: FC = |cx| { - let (caller, name, code) = REFERENCES[cx.selected]; +static ReferenceItem: FC = |cx, props| { + let (caller, name, code) = REFERENCES[props.selected]; // Create the component using the factory API directly let caller_node = LazyNodes::new(move |f| f.component(caller, (), None, &[])); diff --git a/examples/slideshow.rs b/examples/slideshow.rs deleted file mode 100644 index 7aa09b7af..000000000 --- a/examples/slideshow.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Example: Webview Renderer -//! ------------------------- -//! -//! This example shows how to use the dioxus_desktop crate to build a basic desktop application. -//! -//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running -//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events -//! into the native VDom instance. -//! -//! Currently, NodeRefs won't work properly, but all other event functionality will. - -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch(App, |c| c); -} - -static App: FC<()> = |cx| { - let slides = use_state(cx, SlideController::new); - - let slide = match slides.slide_id { - 0 => cx.render(rsx!(Title {})), - 1 => cx.render(rsx!(Slide1 {})), - 2 => cx.render(rsx!(Slide2 {})), - 3 => cx.render(rsx!(Slide3 {})), - _ => cx.render(rsx!(End {})), - }; - - cx.render(rsx! { - div { - style: { - background_color: "red" - } - div { - div { h1 {"my awesome slideshow"} } - div { - button {"<-", onclick: move |_| slides.get_mut().go_forward()} - h3 { "{slides.slide_id}" } - button {"->" onclick: move |_| slides.get_mut().go_backward()} - } - } - {slide} - } - }) -}; - -#[derive(Clone)] -struct SlideController { - slide_id: isize, -} -impl SlideController { - fn new() -> Self { - Self { slide_id: 0 } - } - fn can_go_forward(&self) -> bool { - false - } - fn can_go_backward(&self) -> bool { - true - } - fn go_forward(&mut self) { - if self.can_go_forward() { - self.slide_id += 1; - } - } - fn go_backward(&mut self) { - if self.can_go_backward() { - self.slide_id -= 1; - } - } -} - -const Title: FC<()> = |cx| { - cx.render(rsx! { - div { - h1 { "Title" } - p {} - } - }) -}; -const Slide1: FC<()> = |cx| { - cx.render(rsx! { - div { - h1 { "Slide1" } - p {} - } - }) -}; -const Slide2: FC<()> = |cx| { - cx.render(rsx! { - div { - h1 { "Slide2" } - p {} - } - }) -}; -const Slide3: FC<()> = |cx| { - cx.render(rsx! { - div { - h1 { "Slide3" } - p {} - } - }) -}; -const End: FC<()> = |cx| { - cx.render(rsx! { - div { - h1 { "End" } - p {} - } - }) -}; diff --git a/examples/ssr.rs b/examples/ssr.rs index 9a74031d4..b5d29844d 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -9,7 +9,7 @@ fn main() { println!("{}", ssr::render_vdom(&vdom, |c| c)); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { cx.render(rsx!( div { h1 { "Title" } @@ -21,6 +21,6 @@ static App: FC<()> = |cx| { struct MyProps<'a> { text: &'a str, } -fn App2<'a>(cx: Context<'a, MyProps>) -> DomTree<'a> { +fn App2<'a>(cx: Context<'a>, props: &'a MyProps) -> DomTree<'a> { None } diff --git a/examples/tailwind.rs b/examples/tailwind.rs index c4149faeb..406f56613 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -14,7 +14,7 @@ fn main() { const STYLE: &str = "body {overflow:hidden;}"; -pub static App: FC<()> = |cx| { +pub static App: FC<()> = |cx, props| { cx.render(rsx!( div { class: "overflow-hidden" style { "{STYLE}" } @@ -30,7 +30,7 @@ pub static App: FC<()> = |cx| { )) }; -pub static Header: FC<()> = |cx| { +pub static Header: FC<()> = |cx, props| { cx.render(rsx! { div { header { class: "text-gray-400 bg-gray-900 body-font" @@ -56,7 +56,7 @@ pub static Header: FC<()> = |cx| { }) }; -pub static Hero: FC<()> = |cx| { +pub static Hero: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -94,7 +94,7 @@ pub static Hero: FC<()> = |cx| { } }) }; -pub static Entry: FC<()> = |cx| { +pub static Entry: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -107,7 +107,7 @@ pub static Entry: FC<()> = |cx| { }) }; -pub static StacksIcon: FC<()> = |cx| { +pub static StacksIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { // xmlns: "http://www.w3.org/2000/svg" @@ -122,7 +122,7 @@ pub static StacksIcon: FC<()> = |cx| { } )) }; -pub static RightArrowIcon: FC<()> = |cx| { +pub static RightArrowIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { fill: "none" diff --git a/examples/testbed.rs b/examples/testbed.rs deleted file mode 100644 index 1eadcac53..000000000 --- a/examples/testbed.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::cell::Cell; - -use dioxus::prelude::*; -use dioxus_core::{ - nodes::{VElement, VText}, - ElementId, -}; - -fn main() { - env_logger::init(); - dioxus::desktop::launch(Example, |c| c); -} - -const STYLE: &str = r#" -body {background-color: powderblue;} -h1 {color: blue;} -p {color: red;} -"#; - -const Example: FC<()> = |cx| { - cx.render(rsx! { - Fragment { - Fragment { - Fragment { - "h1" - div { - - } - } - "h2" - } - "h3" - } - "h4" - div { "h5" } - button { } - Child {} - }) -}; - -const Child: FC<()> = |cx| { - cx.render(rsx!( - h1 {"1" } - h1 {"2" } - h1 {"3" } - h1 {"4" } - )) -}; - -// this is a bad case that hurts our subtree memoization :( -const AbTest: FC<()> = |cx| { - if 1 == 2 { - cx.render(rsx!( - h1 {"1"} - h1 {"2"} - h1 {"3"} - h1 {"4"} - )) - } else { - cx.render(rsx!( - h1 {"1"} - h1 {"2"} - h2 {"basd"} - h1 {"4"} - )) - } -}; diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 5add6a446..c094d889a 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -22,7 +22,7 @@ pub struct TodoItem { } const STYLE: &str = include_str!("./_examples/todomvc/style.css"); -const App: FC<()> = |cx| { +const App: FC<()> = |cx, props| { let draft = use_state(cx, || "".to_string()); let todos = use_state(cx, || HashMap::>::new()); let filter = use_state(cx, || FilterState::All); @@ -48,63 +48,60 @@ const App: FC<()> = |cx| { _ => "items", }; - cx.render(rsx! { - div { id: "app" - style {"{STYLE}"} - div { - header { class: "header" - h1 {"todos"} - input { - class: "new-todo" - placeholder: "What needs to be done?" - value: "{draft}" - oninput: move |evt| draft.set(evt.value()) + rsx!(cx, div { id: "app" + style {"{STYLE}"} + div { + header { class: "header" + h1 {"todos"} + input { + class: "new-todo" + placeholder: "What needs to be done?" + value: "{draft}" + oninput: move |evt| draft.set(evt.value()) + } + } + {todolist} + {(!todos.is_empty()).then(|| rsx!( + footer { + span { strong {"{items_left}"} span {"{item_text} left"} } + ul { class: "filters" + li { class: "All", a { href: "", onclick: move |_| filter.set(FilterState::All), "All" }} + li { class: "Active", a { href: "active", onclick: move |_| filter.set(FilterState::Active), "Active" }} + li { class: "Completed", a { href: "completed", onclick: move |_| filter.set(FilterState::Completed), "Completed" }} } } - {todolist} - {(!todos.is_empty()).then(|| rsx!( - footer { - span { strong {"{items_left}"} span {"{item_text} left"} } - ul { class: "filters" - li { class: "All", a { href: "", onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { href: "active", onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { href: "completed", onclick: move |_| filter.set(FilterState::Completed), "Completed" }} - } - } - ))} - } - footer { class: "info" - p {"Double-click to edit a todo"} - p { "Created by ", a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }} - p { "Part of ", a { "TodoMVC", href: "http://todomvc.com" }} - } + ))} + } + footer { class: "info" + p {"Double-click to edit a todo"} + p { "Created by ", a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }} + p { "Part of ", a { "TodoMVC", href: "http://todomvc.com" }} } }) }; #[derive(PartialEq, Props)] pub struct TodoEntryProps { - todo: std::rc::Rc, + todo: Rc, } -pub fn TodoEntry(cx: Context) -> DomTree { - let TodoEntryProps { todo } = *cx; +pub fn TodoEntry<'a>(cx: Context<'a>, props: &TodoEntryProps) -> DomTree<'a> { let is_editing = use_state(cx, || false); - let contents = ""; + let contents = use_state(cx, || String::from("")); + let todo = &props.todo; - cx.render(rsx! ( - li { - "{todo.id}" - input { - class: "toggle" - r#type: "checkbox" - "{todo.checked}" - } - {is_editing.then(|| rsx!{ - input { - value: "{contents}" - } - })} + rsx!(cx, li { + "{todo.id}" + input { + class: "toggle" + r#type: "checkbox" + "{todo.checked}" } - )) + {is_editing.then(|| rsx!{ + input { + value: "{contents}" + oninput: move |evt| contents.set(evt.value()) + } + })} + }) } diff --git a/examples/web_tick.rs b/examples/web_tick.rs index e82f131ec..82fe47db4 100644 --- a/examples/web_tick.rs +++ b/examples/web_tick.rs @@ -19,7 +19,7 @@ fn main() { dioxus::web::launch(App, |c| c); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let mut rng = SmallRng::from_entropy(); let rows = (0..1_000).map(|f| { let label = Label::new(&mut rng); @@ -45,11 +45,11 @@ struct RowProps { row_id: usize, label: Label, } -fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { - let [adj, col, noun] = cx.label.0; +fn Row<'a>(cx: Context<'a>, props: &'a RowProps) -> DomTree<'a> { + let [adj, col, noun] = props.label.0; cx.render(rsx! { tr { - td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", "{props.row_id}" } td { class:"col-md-1", onclick: move |_| { /* run onselect */ } a { class: "lbl", "{adj}" "{col}" "{noun}" } } diff --git a/examples/webview.rs b/examples/webview.rs deleted file mode 100644 index 8759fc733..000000000 --- a/examples/webview.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Example: Webview Renderer -//! ------------------------- -//! -//! This example shows how to use the dioxus_desktop crate to build a basic desktop application. -//! -//! Under the hood, the dioxus_desktop crate bridges a native Dioxus VirtualDom with a custom prebuit application running -//! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events -//! into the native VDom instance. -//! -//! Currently, NodeRefs won't work properly, but all other event functionality will. -#![allow(non_upper_case_globals, non_snake_case)] - -use dioxus::{events::on::MouseEvent, prelude::*}; - -fn main() -> anyhow::Result<()> { - env_logger::init(); - dioxus::desktop::launch(App, |c| c) -} - -static App: FC<()> = |cx| { - let state = use_state(cx, || String::from("hello")); - let clear_text = state == "hello"; - - dbg!("rednering parent"); - cx.render(rsx! { - div { - h1 {"{state}"} - CalculatorKey { name: "key-clear", onclick: move |_| state.get_mut().push_str("hello"), "{clear_text}" } - CalculatorKey { name: "key-sign", onclick: move |_| { state.get_mut().pop(); }, "ยฑ"} - } - }) -}; - -#[derive(Props)] -struct CalculatorKeyProps<'a> { - name: &'static str, - onclick: &'a dyn Fn(MouseEvent), -} - -fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> DomTree<'a> { - cx.render(rsx! { - button { - class: "calculator-key {cx.name}" - onclick: {cx.onclick} - {cx.children()} - } - }) -} diff --git a/examples/webview_web.rs b/examples/webview_web.rs index b897ffb5a..9ff57388d 100644 --- a/examples/webview_web.rs +++ b/examples/webview_web.rs @@ -16,7 +16,7 @@ fn main() { dioxus::web::launch(App, |c| c); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); cx.render(rsx! { diff --git a/notes/Parity.md b/notes/Parity.md index 11655c2e7..5cc0851c9 100644 --- a/notes/Parity.md +++ b/notes/Parity.md @@ -12,7 +12,7 @@ https://github.com/rustwasm/gloo For example, resize observer would function like this: ```rust -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ let observer = use_resize_observer(); cx.render(rsx!( diff --git a/notes/SOLVEDPROBLEMS.md b/notes/SOLVEDPROBLEMS.md index 7f501822e..377d6f221 100644 --- a/notes/SOLVEDPROBLEMS.md +++ b/notes/SOLVEDPROBLEMS.md @@ -153,13 +153,13 @@ Notice that LiveComponent receivers (the client-side interpretation of a LiveCom The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like: ```rust -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ html! {
"blah"
} }; // expands to... -pub static Example: FC<()> = |cx| { +pub static Example: FC<()> = |cx, props|{ // This function converts a Fn(allocator) -> DomTree closure to a VNode struct that will later be evaluated. html_macro_to_vnodetree(move |allocator| { let mut node0 = allocator.alloc(VElement::div); @@ -313,7 +313,7 @@ Here's how react does it: Any "dirty" node causes an entire subtree render. Calling "setState" at the very top will cascade all the way down. This is particularly bad for this component design: ```rust -static APP: FC<()> = |cx| { +static APP: FC<()> = |cx, props|{ let title = use_context(Title); cx.render(html!{
@@ -334,7 +334,7 @@ static APP: FC<()> = |cx| {
}) }; -static HEAVY_LIST: FC<()> = |cx| { +static HEAVY_LIST: FC<()> = |cx, props|{ cx.render({ {0.100.map(i => )} }) @@ -378,7 +378,7 @@ struct Props { } -static Component: FC = |cx| { +static Component: FC = |cx, props|{ } ``` diff --git a/packages/core-macro/examples/fc.rs b/packages/core-macro/examples/fc.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/core-macro/examples/fc.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs index d80c0cc35..525b3c8b0 100644 --- a/packages/core-macro/examples/prop_test.rs +++ b/packages/core-macro/examples/prop_test.rs @@ -20,5 +20,5 @@ struct SomeProps { /// This implementation does not require a "PartialEq" because it does not memoize #[derive(dioxus_core_macro::Props)] struct SomePropsTwo<'a> { - a: &'a str, + _a: &'a str, } diff --git a/packages/core-macro/examples/rsxt.rs b/packages/core-macro/examples/rsxt.rs deleted file mode 100644 index 2a6434b5d..000000000 --- a/packages/core-macro/examples/rsxt.rs +++ /dev/null @@ -1,125 +0,0 @@ -pub mod dioxus { - pub mod builder { - pub struct Builder; - - struct AttrVal; - - impl Into for &'static str { - fn into(self) -> AttrVal { - todo!() - } - } - - impl Into for String { - fn into(self) -> AttrVal { - todo!() - } - } - // impl From for AttrVal { - // fn from(_: T) -> Self { - // todo!() - // } - // } - - impl Builder { - // fn attr(mut self, key: &str, value: impl Into) -> Self { - pub fn attr(self, _key: &str, _value: T) -> Self { - Self - } - - pub fn on(self, _key: &str, _value: T) -> Self { - Self - } - - pub fn finish(self) { - // Self - } - } - - pub struct Bump; - pub fn div(_bump: &Bump) -> Builder { - todo!() - } - pub fn h1(_bump: &Bump) -> Builder { - todo!() - } - pub fn h2(_bump: &Bump) -> Builder { - todo!() - } - } -} - -pub fn main() { - // render(rsx! { - // div { // we can actually support just a list of nodes too - // h1 {"Hello Dioxus"} - // p {"This is a beautful app you're building"} - // section { - // "custom section to the rescue", - // class: "abc123" - // } - // span { - // class: "abc123" - // "Try backwards too." - // "Anything goes!" - // "As long as it's within the rules" - // {0..10.map(|f| rsx!{ - // div { - // h3 {"totally okay to drop in iterators and expressions"} - // p {"however, debug information is lost"} - // } - // })} - // } - // span { - // "Feel free" - // class: "abc123" - // "To mix to your heart's content" - // } - // span { class: "some-very-long-and-tedious-class-name-is-now-separated" - // "Very ergonomic" - // } - // span { "Innovative design ๐Ÿ› " - // class: "some-very-long-and-tedious-class-name-is-now-separated" - // } - // } - // }); - - let _g = String::from("asd"); - - // let lazy = rsx! { - // div { - // a: "asd", - // a: "asd", - // a: "asd", - // a: "asd", - // a: "asd", - // // a: {rsx!{ h1 {"hello world"} }}, // include - // a: {&g}, - // b: {1 + 2}, - // onclick: {move |e: ()| { - // println!("hello world!") - // }}, - // div { - // a: "asd" - // div { - // div { - // div { - - // } - // } - // } - // } - // h1 { - - // } - // h2 { - // "child" - // } - // "Childnode" - // } - // }; - - // render(lazy); -} - -fn render(_f: impl Fn(&dioxus::builder::Bump)) {} diff --git a/packages/core-macro/src/ifmt.rs b/packages/core-macro/src/ifmt.rs index 816c1f400..5bac539d2 100644 --- a/packages/core-macro/src/ifmt.rs +++ b/packages/core-macro/src/ifmt.rs @@ -237,8 +237,8 @@ where // # Safety // // - This is the canonical example of using `ManuallyDrop`. - let value = ManuallyDrop::into_inner(ptr::read(&mut self.0)); - let drop = ManuallyDrop::into_inner(ptr::read(&mut self.1)); + let value = ManuallyDrop::into_inner(ptr::read(&self.0)); + let drop = ManuallyDrop::into_inner(ptr::read(&self.1)); drop(value); } } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 8d124800a..f4763009d 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -30,7 +30,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// /// ## Complete Reference Guide: /// ``` -/// const Example: FC<()> = |cx| { +/// const Example: FC<()> = |cx, props|{ /// let formatting = "formatting!"; /// let formatting_tuple = ("a", "b"); /// let lazy_fmt = format_args!("lazily formatted text"); @@ -87,7 +87,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// // To fix this, call "render" method or use the "in" syntax to produce VNodes. /// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) /// {match true { -/// true => rsx!(in cx, h1 {"Top text"}), +/// true => rsx!(cx, h1 {"Top text"}), /// false => cx.render(rsx!( h1 {"Bottom text"})) /// }} /// @@ -98,9 +98,9 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// /// // True conditions need to be rendered (same reasons as matching) /// {if true { -/// rsx!(in cx, h1 {"Top text"}) +/// rsx!(cx, h1 {"Top text"}) /// } else { -/// rsx!(in cx, h1 {"Bottom text"}) +/// rsx!(cx, h1 {"Bottom text"}) /// }} /// /// // returning "None" is a bit noisy... but rare in practice diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs index bfcfbc374..ad535ef31 100644 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ b/packages/core-macro/src/rsx/ambiguous.rs @@ -86,11 +86,11 @@ impl Parse for AmbiguousElement { if first_char.is_ascii_uppercase() { input .parse::>() - .map(|c| AmbiguousElement::Component(c)) + .map(AmbiguousElement::Component) } else { input .parse::>() - .map(|c| AmbiguousElement::Element(c)) + .map(AmbiguousElement::Element) } } else { Err(Error::new(input.span(), "Not a valid Html tag")) diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index 8e0e97d46..718c1c83d 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -39,8 +39,7 @@ impl Parse for RsxBody { } fn try_parse_custom_context(input: ParseStream) -> Result> { - let res = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { - let _ = input.parse::()?; + let res = if input.peek(Ident) && input.peek2(Token![,]) { let name = input.parse::()?; input.parse::()?; Some(name) diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 4d9a2f09e..451a3d1f9 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -270,7 +270,7 @@ fn parse_rsx_element_field( name, value: ty, namespace: None, - element_name: element_name.clone(), + element_name, }); return Ok(()); } diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/core-macro/src/rsxtemplate.rs index 9222d7117..1f712997d 100644 --- a/packages/core-macro/src/rsxtemplate.rs +++ b/packages/core-macro/src/rsxtemplate.rs @@ -72,7 +72,7 @@ impl ToTokens for RsxTemplate { // // create a lazy tree that accepts a bump allocator // let final_tokens = quote! { - // dioxus::prelude::LazyNodes::new(move |cx| { + // dioxus::prelude::LazyNodes::new(move |cx, props|{ // let bump = &cx.bump(); // #new_toks diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 79505b018..1c68f97cc 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } diff --git a/packages/core/README.md b/packages/core/README.md index 62a876453..0cb1410fd 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -15,7 +15,7 @@ Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus - Preact: approach for normalization and ref - Yew: passion and inspiration โค๏ธ -Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include: +Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features: - managed lifetimes for borrowed data - suspended nodes (task/fiber endpoints) for asynchronous vnodes @@ -24,7 +24,7 @@ Dioxus-core leverages some really cool techniques and hits a very high level of - slab allocator for scopes - mirrored-slab approach for remote vdoms -There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is likely that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. +There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is possible that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. All in all, Dioxus treats memory as an incredibly valuable resource. Combined with the memory-efficient footprint of WASM compilation, Dioxus apps can scale to thousands of components and still stay snappy and respect your RAM usage. diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs index 294a9a952..e69fd9511 100644 --- a/packages/core/benches/jsframework.rs +++ b/packages/core/benches/jsframework.rs @@ -22,7 +22,7 @@ criterion_group!(mbenches, create_rows); criterion_main!(mbenches); fn create_rows(c: &mut Criterion) { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, _| { let mut rng = SmallRng::from_entropy(); let rows = (0..10_000).map(|f| { let label = Label::new(&mut rng); @@ -56,11 +56,11 @@ struct RowProps { row_id: usize, label: Label, } -fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { - let [adj, col, noun] = cx.label.0; +fn Row<'a>(cx: Context<'a>, props: &RowProps) -> DomTree<'a> { + let [adj, col, noun] = props.label.0; cx.render(rsx! { tr { - td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", "{props.row_id}" } td { class:"col-md-1", onclick: move |_| { /* run onselect */ } a { class: "lbl", "{adj}" "{col}" "{noun}" } } diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index 95a8469ff..bb170873e 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -1,8 +1,12 @@ use dioxus_core::prelude::*; -fn main() {} +fn main() { + let mut dom = VirtualDom::new(EXAMPLE); + dom.rebuild(); + println!("{}", dom); +} -pub static Example: FC<()> = |cx| { +pub static EXAMPLE: FC<()> = |cx, _| { let list = (0..10).map(|_f| LazyNodes::new(move |_f| todo!())); cx.render(LazyNodes::new(move |cx| { diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index 73a83b456..6575afe5f 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -1,39 +1,15 @@ use dioxus_core::prelude::*; -fn main() {} - -const App: FC<()> = |cx| { - // create a new future - let _fut = cx.use_hook( - |_| { - // - async { loop {} } - // Box::pin(async { loop {} }) as Pin>> - }, - |f| f, - |_| {}, - ); - // let g = unsafe { Pin::new_unchecked(fut) }; - - // cx.submit_task(fut); - - todo!() -}; - -const Task: FC<()> = |cx| { - let (_task, _res) = use_task(cx, || async { true }); - // task.pause(); - // task.restart(); - // task.stop(); - // task.drop(); - - // - - let _s = use_task(cx, || async { "hello world".to_string() }); - - todo!() -}; - -fn use_mut(_cx: Context

, _f: impl FnOnce() -> T) -> &mut T { - todo!() +fn main() { + let mut dom = VirtualDom::new(App); + dom.rebuild(); } + +const App: FC<()> = |cx, props| { + let id = cx.scope_id(); + cx.submit_task(Box::pin(async move { id })); + + let (handle, contents) = use_task(cx, || async { "hello world".to_string() }); + + todo!() +}; diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index e30447548..a11f55225 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -20,7 +20,7 @@ struct ListItem { age: u32, } -fn app(cx: Context) -> DomTree { +fn app<'a>(cx: Context<'a>, props: &AppProps) -> DomTree<'a> { // let (val, set_val) = use_state_classic(cx, || 0); cx.render(LazyNodes::new(move |_nodecx| { @@ -56,7 +56,7 @@ struct ChildProps { item_handler: Rc, } -fn ChildItem<'a>(cx: Context<'a, ChildProps>) -> DomTree { +fn ChildItem<'a>(cx: Context<'a>, props: &'a ChildProps) -> DomTree<'a> { cx.render(LazyNodes::new(move |__cx| todo!())) } diff --git a/packages/core/examples/contextapi.rs b/packages/core/examples/contextapi.rs index 61dc79837..6c90a60c7 100644 --- a/packages/core/examples/contextapi.rs +++ b/packages/core/examples/contextapi.rs @@ -6,7 +6,7 @@ struct SomeContext { } #[allow(unused)] -static Example: FC<()> = |cpt| { +static Example: FC<()> = |cx, props| { todo!() // let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap()); diff --git a/packages/core/examples/fragment_from_iter.rs b/packages/core/examples/fragment_from_iter.rs index 58c4df70f..ae7d761e8 100644 --- a/packages/core/examples/fragment_from_iter.rs +++ b/packages/core/examples/fragment_from_iter.rs @@ -2,7 +2,7 @@ use dioxus_core::prelude::*; fn main() {} -fn app(cx: Context<()>) -> DomTree { +fn app<'a>(cx: Context<'a>, props: &()) -> DomTree<'a> { let vak = use_suspense( cx, || async {}, diff --git a/packages/core/examples/jsframework.rs b/packages/core/examples/jsframework.rs index 79400afd1..902869f43 100644 --- a/packages/core/examples/jsframework.rs +++ b/packages/core/examples/jsframework.rs @@ -10,7 +10,7 @@ fn main() { assert!(g.edits.len() > 1); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props| { let mut rng = SmallRng::from_entropy(); let rows = (0..10_000).map(|f| { let label = Label::new(&mut rng); @@ -35,12 +35,12 @@ struct RowProps { row_id: usize, label: Label, } -fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree { +fn Row<'a>(cx: Context<'a>, props: &'a RowProps) -> DomTree<'a> { cx.render(rsx! { tr { - td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", "{props.row_id}" } td { class:"col-md-1", onclick: move |_| { /* run onselect */ } - a { class: "lbl", "{cx.label}" } + a { class: "lbl", "{props.label}" } } td { class: "col-md-1" a { class: "remove", onclick: move |_| {/* remove */} diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs index c5d643df5..7175a167d 100644 --- a/packages/core/examples/vdom_usage.rs +++ b/packages/core/examples/vdom_usage.rs @@ -4,12 +4,13 @@ use dioxus_core::prelude::*; #[async_std::main] async fn main() { - static App: FC<()> = |cx| cx.render(LazyNodes::new(|f| f.text(format_args!("hello")))); + static App: FC<()> = |cx, props| cx.render(LazyNodes::new(|f| f.text(format_args!("hello")))); let mut dom = VirtualDom::new(App); dom.rebuild(); let deadline = async_std::task::sleep(Duration::from_millis(50)); - let _fut = dom.run_with_deadline(deadline); + + // let _fut = dom.run_with_deadline(|| deadline.); } diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 8cb6d6734..a696d82b5 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -14,6 +14,7 @@ pub(crate) struct BumpFrame { pub bump: Bump, pub(crate) head_node: VNode<'static>, + #[cfg(test)] // used internally for debugging _name: &'static str, } @@ -23,11 +24,15 @@ impl ActiveFrame { let frame_a = BumpFrame { bump: Bump::new(), head_node: NodeFactory::unstable_place_holder(), + + #[cfg(test)] _name: "wip", }; let frame_b = BumpFrame { bump: Bump::new(), head_node: NodeFactory::unstable_place_holder(), + + #[cfg(test)] _name: "fin", }; Self { diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index daf5281c4..e7032e939 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -39,7 +39,7 @@ impl<'a> Iterator for RealChildIterator<'a> { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next should_pop = true; - returned_node = Some(&*node); + returned_node = Some(node); } // If we get a fragment we push the next child @@ -64,7 +64,6 @@ impl<'a> Iterator for RealChildIterator<'a> { .scopes .get_scope(sc.associated_scope.get().unwrap()) .unwrap(); - // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap(); // Simply swap the current node on the stack with the root of the component *node = scope.frames.fin_head(); diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index ee25bd5bd..0ec306229 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -7,6 +7,32 @@ use crate::innerlude::{Context, DomTree, LazyNodes, FC}; +/// Create inline fragments using Component syntax. +/// +/// Fragments capture a series of children without rendering extra nodes. +/// +/// # Example +/// +/// ```rust +/// rsx!{ +/// Fragment { key: "abc" } +/// } +/// ``` +/// +/// # Details +/// +/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +/// Try to avoid nesting fragments if you can. There is no protection against infinitely nested fragments. +/// +/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +/// +/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +/// +#[allow(non_upper_case_globals, non_snake_case)] +pub fn Fragment<'a>(cx: Context<'a>, _: &'a ()) -> DomTree<'a> { + cx.render(LazyNodes::new(|f| f.fragment_from_iter(cx.children()))) +} + /// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus /// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the /// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. @@ -44,7 +70,7 @@ pub trait Properties: Sized { const IS_STATIC: bool; fn builder() -> Self::Builder; - /// Memoization can only happen if the props are 'static + /// Memoization can only happen if the props are valid for the 'static lifetime /// The user must know if their props are static, but if they make a mistake, UB happens /// Therefore it's unsafe to memeoize. unsafe fn memoize(&self, other: &Self) -> bool; @@ -65,9 +91,7 @@ impl Properties for () { pub struct EmptyBuilder; impl EmptyBuilder { #[inline] - pub fn build(self) -> () { - () - } + pub fn build(self) {} } /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern @@ -75,24 +99,3 @@ impl EmptyBuilder { pub fn fc_to_builder(_: FC) -> T::Builder { T::builder() } - -/// Create inline fragments -/// -/// Fragments capture a series of children without rendering extra nodes. -/// -/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. -/// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash. -/// -/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. -/// -/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. -/// -/// ```rust -/// rsx!{ -/// Fragment { key: "abc" } -/// } -/// ``` -#[allow(non_upper_case_globals, non_snake_case)] -pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> { - cx.render(LazyNodes::new(move |f| f.fragment_from_iter(cx.children()))) -} diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 21d89436e..9d16c9c01 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -27,31 +27,27 @@ use std::{any::TypeId, ops::Deref, rc::Rc}; /// } /// } /// ``` -pub struct Context<'src, T> { - pub props: &'src T, +pub struct Context<'src> { pub scope: &'src Scope, } -impl<'src, T> Copy for Context<'src, T> {} -impl<'src, T> Clone for Context<'src, T> { +impl<'src> Copy for Context<'src> {} +impl<'src> Clone for Context<'src> { fn clone(&self) -> Self { - Self { - props: self.props, - scope: self.scope, - } + Self { scope: self.scope } } } // We currently deref to props, but it might make more sense to deref to Scope? // This allows for code that takes cx.xyz instead of cx.props.xyz -impl<'a, T> Deref for Context<'a, T> { - type Target = &'a T; +impl<'a> Deref for Context<'a> { + type Target = &'a Scope; fn deref(&self) -> &Self::Target { - &self.props + &self.scope } } -impl<'src, P> Context<'src, P> { +impl<'src> Context<'src> { /// Access the children elements passed into the component /// /// This enables patterns where a component is passed children from its parent. @@ -69,7 +65,7 @@ impl<'src, P> Context<'src, P> { /// ## Example /// /// ```rust - /// const App: FC<()> = |cx| { + /// const App: FC<()> = |cx, props|{ /// cx.render(rsx!{ /// CustomCard { /// h1 {} @@ -78,7 +74,7 @@ impl<'src, P> Context<'src, P> { /// }) /// } /// - /// const CustomCard: FC<()> = |cx| { + /// const CustomCard: FC<()> = |cx, props|{ /// cx.render(rsx!{ /// div { /// h1 {"Title card"} @@ -98,15 +94,26 @@ impl<'src, P> Context<'src, P> { /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id - /// pub fn schedule_update(&self) -> Rc { self.scope.memoized_updater.clone() } - pub fn prepare_update(&self) -> Rc { + /// Schedule an update for any component given its ScopeId. + /// + /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method. + /// + /// This method should be used when you want to schedule an update for a component + pub fn schedule_update_any(&self) -> Rc { self.scope.shared.schedule_any_immediate.clone() } + /// Get the [`ScopeId`] of a mounted component. + /// + /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. + pub fn scope_id(&self) -> ScopeId { + self.scope.our_arena_idx + } + /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. /// /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. @@ -151,22 +158,81 @@ impl<'src, P> Context<'src, P> { (self.scope.shared.submit_task)(task) } - /// Add a state globally accessible to child components via tree walking - pub fn add_shared_state(self, val: T) { - self.scope - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Rc::new(val)) - .map(|_| { - log::warn!("A shared state was replaced with itself. This is does not result in a panic, but is probably not what you are trying to do"); - }); + /// This hook enables the ability to expose state to children further down the VirtualDOM Tree. + /// + /// This is a hook, so it should not be called conditionally! + /// + /// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`) + /// so don't put it in a conditional. + /// + /// When the component is dropped, so is the context. Be aware of this behavior when consuming + /// the context via Rc/Weak. + /// + /// # Example + /// + /// ``` + /// struct SharedState(&'static str); + /// + /// static App: FC<()> = |cx, props|{ + /// cx.use_provide_state(|| SharedState("world")); + /// rsx!(cx, Child {}) + /// } + /// + /// static Child: FC<()> = |cx, props|{ + /// let state = cx.use_consume_state::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn use_provide_state(self, init: F) -> &'src Rc + where + T: 'static, + F: FnOnce() -> T, + { + let is_initialized = self.use_hook( + |_| false, + |s| { + let i = *s; + *s = true; + i + }, + |_| {}, + ); + + if !is_initialized { + let existing = self + .scope + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(init())); + + if existing.is_some() { + log::warn!( + "A shared state was replaced with itself. \ + This is does not result in a panic, but is probably not what you are trying to do" + ); + } + } + + self.use_consume_state().unwrap() } - pub fn consume_shared_state(self) -> Option> { - let getter = &self.scope.shared.get_shared_context; - let ty = TypeId::of::(); - let idx = self.scope.our_arena_idx; - getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated")) + /// Uses a context, storing the cached value around + /// + /// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a + /// context was added later. This allows using another hook as a fallback + /// + pub fn use_consume_state(self) -> Option<&'src Rc> { + struct UseContextHook(Option>); + self.use_hook( + move |_| { + let getter = &self.scope.shared.get_shared_context; + let ty = TypeId::of::(); + let idx = self.scope.our_arena_idx; + UseContextHook(getter(idx, ty).map(|f| f.downcast().unwrap())) + }, + move |hook| hook.0.as_ref(), + |_| {}, + ) } /// Store a value between renders @@ -177,12 +243,15 @@ impl<'src, P> Context<'src, P> { /// - Runner: closure used to output a value every time the hook is used /// - Cleanup: closure used to teardown the hook once the dom is cleaned up /// + /// + /// # Example + /// /// ```ignore /// // use_ref is the simplest way of storing a value between renders - /// pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> { + /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell { /// use_hook( /// || Rc::new(RefCell::new(initial_value())), - /// |state| state.clone(), + /// |state| state, /// |_| {}, /// ) /// } @@ -191,29 +260,31 @@ impl<'src, P> Context<'src, P> { self, initializer: Init, runner: Run, - _cleanup: Cleanup, + cleanup: Cleanup, ) -> Output where State: 'static, Output: 'src, Init: FnOnce(usize) -> State, Run: FnOnce(&'src mut State) -> Output, - Cleanup: FnOnce(State), + Cleanup: FnOnce(&mut State) + 'static, { // If the idx is the same as the hook length, then we need to add the current hook if self.scope.hooks.at_end() { - let new_state = initializer(self.scope.hooks.len()); - self.scope.hooks.push(new_state); + self.scope.hooks.push_hook( + initializer(self.scope.hooks.len()), + Box::new(|raw| cleanup(raw.downcast_mut::().unwrap())), + ); } - const ERR_MSG: &str = r###" + runner(self.scope.hooks.next::().expect(HOOK_ERR_MSG)) + } +} + +const HOOK_ERR_MSG: &str = r###" Unable to retrive the hook that was initialized in this index. Consult the `rules of hooks` to understand how to use hooks properly. You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. Any function prefixed with "use" should not be called conditionally. "###; - - runner(self.scope.hooks.next::().expect(ERR_MSG)) - } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index bafdfea35..e29b5e18e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -109,6 +109,18 @@ pub(crate) struct DiffMachine<'bump> { pub mutations: Mutations<'bump>, pub stack: DiffStack<'bump>, pub seen_scopes: FxHashSet, + pub cfg: DiffCfg, +} + +pub(crate) struct DiffCfg { + pub force_diff: bool, +} +impl Default for DiffCfg { + fn default() -> Self { + Self { + force_diff: Default::default(), + } + } } /// a "saved" form of a diff machine @@ -124,10 +136,12 @@ impl<'a> SavedDiffWork<'a> { pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> { std::mem::transmute(self) } - pub unsafe fn promote<'b>(self, vdom: &'b mut ResourcePool) -> DiffMachine<'b> { + + pub unsafe fn promote<'b>(self, vdom: &'b ResourcePool) -> DiffMachine<'b> { let extended: SavedDiffWork<'b> = std::mem::transmute(self); DiffMachine { vdom, + cfg: DiffCfg::default(), mutations: extended.mutations, stack: extended.stack, seen_scopes: extended.seen_scopes, @@ -136,10 +150,11 @@ impl<'a> SavedDiffWork<'a> { } impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { + pub(crate) fn new(mutations: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { Self { + mutations, + cfg: DiffCfg::default(), stack: DiffStack::new(), - mutations: edits, vdom: shared, seen_scopes: FxHashSet::default(), } @@ -156,7 +171,8 @@ impl<'bump> DiffMachine<'bump> { pub fn diff_scope(&mut self, id: ScopeId) { if let Some(component) = self.vdom.get_scope_mut(id) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); - self.stack.push(DiffInstruction::DiffNode { new, old }); + self.stack.push(DiffInstruction::Diff { new, old }); + self.work(|| false); } } @@ -167,27 +183,18 @@ impl<'bump> DiffMachine<'bump> { /// 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, deadline_expired: &mut impl FnMut() -> bool) -> bool { + pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool { while let Some(instruction) = self.stack.pop() { - // defer to individual functions so the compiler produces better code - // large functions tend to be difficult for the compiler to work with match instruction { - DiffInstruction::PopScope => { - self.stack.pop_scope(); - } - - DiffInstruction::DiffNode { old, new, .. } => self.diff_node(old, new), - - DiffInstruction::DiffChildren { old, new } => self.diff_children(old, new), - + DiffInstruction::Diff { old, new } => self.diff_node(old, new), DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => self.mount(and), - - DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node), + DiffInstruction::PrepareMove { node } => self.prepare_move_node(node), + DiffInstruction::PopScope => self.stack.pop_off_scope(), }; if deadline_expired() { + log::debug!("Deadling expired before we could finished!"); return false; } } @@ -210,24 +217,31 @@ impl<'bump> DiffMachine<'bump> { MountType::Absorb => { self.stack.add_child_count(nodes_created); } + + MountType::Replace { old } => { + if let Some(old_id) = old.try_mounted_id() { + self.mutations.replace_with(old_id, nodes_created as u32); + } else { + let mut iter = RealChildIterator::new(old, self.vdom); + let first = iter.next().unwrap(); + self.mutations + .replace_with(first.mounted_id(), nodes_created as u32); + self.remove_nodes(iter); + } + } + + MountType::ReplaceByElementId { el } => { + if let Some(old) = el { + self.mutations.replace_with(old, nodes_created as u32); + } + } + MountType::Append => { self.mutations.edits.push(AppendChildren { many: nodes_created as u32, }); } - MountType::Replace { old } => { - let mut iter = RealChildIterator::new(old, self.vdom); - let first = iter.next().unwrap(); - self.mutations - .replace_with(first.mounted_id(), nodes_created as u32); - self.remove_nodes(iter); - } - - MountType::ReplaceByElementId { el: old } => { - self.mutations.replace_with(old, 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); @@ -246,37 +260,40 @@ impl<'bump> DiffMachine<'bump> { fn create_node(&mut self, node: &'bump VNode<'bump>) { match node { - VNode::Text(vtext) => self.create_text_node(vtext), - VNode::Suspended(suspended) => self.create_suspended_node(suspended), - VNode::Anchor(anchor) => self.create_anchor_node(anchor), - VNode::Element(element) => self.create_element_node(element), + VNode::Text(vtext) => self.create_text_node(vtext, node), + VNode::Suspended(suspended) => self.create_suspended_node(suspended, node), + VNode::Anchor(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>) { - let real_id = self.vdom.reserve_node(); + fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { + let real_id = self.vdom.reserve_node(node); self.mutations.create_text_node(vtext.text, real_id); vtext.dom_id.set(Some(real_id)); self.stack.add_child_count(1); } - fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { - let real_id = self.vdom.reserve_node(); + fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) { + let real_id = self.vdom.reserve_node(node); self.mutations.create_placeholder(real_id); + suspended.dom_id.set(Some(real_id)); self.stack.add_child_count(1); + + self.attach_suspended_node_to_scope(suspended); } - fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { - let real_id = self.vdom.reserve_node(); + fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) { + let real_id = self.vdom.reserve_node(node); self.mutations.create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); self.stack.add_child_count(1); } - fn create_element_node(&mut self, element: &'bump VElement<'bump>) { + fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) { let VElement { tag_name, listeners, @@ -287,7 +304,8 @@ impl<'bump> DiffMachine<'bump> { .. } = element; - let real_id = self.vdom.reserve_node(); + let real_id = self.vdom.reserve_node(node); + dom_id.set(Some(real_id)); self.mutations.create_element(tag_name, *namespace, real_id); @@ -311,7 +329,10 @@ impl<'bump> DiffMachine<'bump> { self.mutations.set_attribute(attr); } - if children.len() > 0 { + if !children.is_empty() { + // self.stack.element_id_stack.push(real_id); + // push our element_id onto the stack + // drop our element off the stack self.stack.create_children(children, MountType::Append); } } @@ -321,13 +342,15 @@ impl<'bump> DiffMachine<'bump> { } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let caller = vcomponent.caller.clone(); + let caller = vcomponent.caller; let parent_idx = self.stack.current_scope().unwrap(); let shared = self.vdom.channel.clone(); + // Insert a new scope into our component list let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); + let new_idx = self.vdom.insert_scope_with_key(|new_idx| { let height = parent_scope.height + 1; Scope::new( @@ -376,11 +399,13 @@ impl<'bump> DiffMachine<'bump> { match (old_node, new_node) { // Check the most common cases first (Text(old), Text(new)) => self.diff_text_nodes(old, new), - (Component(old), Component(new)) => self.diff_component_nodes(old, new), + (Component(old), Component(new)) => { + self.diff_component_nodes(old_node, new_node, old, new) + } (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), - (Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()), - (Element(old), Element(new)) => self.diff_element_nodes(old, new), + (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new), + (Element(old), Element(new)) => self.diff_element_nodes(old, new, new_node), // Anything else is just a basic replace and create ( @@ -393,19 +418,24 @@ impl<'bump> DiffMachine<'bump> { } fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { - let root = old.dom_id.get().unwrap(); + if let Some(root) = old.dom_id.get() { + if old.text != new.text { + self.mutations.push_root(root); + self.mutations.set_text(new.text); + self.mutations.pop(); + } - if old.text != new.text { - self.mutations.push_root(root); - self.mutations.set_text(new.text); - self.mutations.pop(); + new.dom_id.set(Some(root)); } - - new.dom_id.set(Some(root)); } - fn diff_element_nodes(&mut self, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>) { - let root = old.dom_id.get().unwrap(); + fn diff_element_nodes( + &mut self, + old: &'bump VElement<'bump>, + new: &'bump VElement<'bump>, + new_node: &'bump VNode<'bump>, + ) { + let root = old.dom_id.get(); // 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 @@ -417,21 +447,24 @@ impl<'bump> DiffMachine<'bump> { self.stack.push_nodes_created(0); self.stack.push(DiffInstruction::Mount { and: MountType::ReplaceByElementId { - el: old.dom_id.get().unwrap(), + el: old.dom_id.get(), }, }); - self.create_element_node(new); + self.create_element_node(new, new_node); return; } - new.dom_id.set(Some(root)); + new.dom_id.set(root); // Don't push the root if we don't have to let mut has_comitted = false; let mut please_commit = |edits: &mut Vec| { if !has_comitted { has_comitted = true; - edits.push(PushRoot { id: root.as_u64() }); + log::info!("planning on committing... {:#?}, {:#?}", old, new); + edits.push(PushRoot { + id: root.unwrap().as_u64(), + }); } }; @@ -443,13 +476,17 @@ impl<'bump> DiffMachine<'bump> { // 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()) { + log::info!("checking attribute"); if old_attr.value != new_attr.value { please_commit(&mut self.mutations.edits); self.mutations.set_attribute(new_attr); + } else if new_attr.is_volatile { + log::debug!("setting due to volatile atttributes"); + please_commit(&mut self.mutations.edits); + self.mutations.set_attribute(new_attr); } } } else { - // TODO: provide some sort of report on how "good" the diffing was please_commit(&mut self.mutations.edits); for attribute in old.attributes { self.mutations.remove_attribute(attribute); @@ -467,7 +504,6 @@ impl<'bump> DiffMachine<'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() { let scope = self.vdom.get_scope(cur_scope_id).unwrap(); @@ -487,7 +523,7 @@ impl<'bump> DiffMachine<'bump> { self.mutations.remove_event_listener(listener.event); } for listener in new.listeners { - listener.mounted_node.set(Some(root)); + listener.mounted_node.set(root); self.mutations.new_event_listener(listener, cur_scope_id); self.attach_listener_to_scope(listener, scope); } @@ -503,6 +539,9 @@ impl<'bump> DiffMachine<'bump> { fn diff_component_nodes( &mut self, + old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + old: &'bump VComponent<'bump>, new: &'bump VComponent<'bump>, ) { @@ -519,20 +558,16 @@ impl<'bump> DiffMachine<'bump> { // make sure the component's caller function is up to date let scope = self.vdom.get_scope_mut(scope_addr).unwrap(); - scope.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children)); + scope.update_scope_dependencies(new.caller, ScopeChildren(new.children)); // React doesn't automatically memoize, but we do. - let compare = old.comparator.unwrap(); + let props_are_the_same = old.comparator.unwrap(); - match compare(new) { - true => { - // the props are the same... - } - false => { - // the props are different... - if scope.run_scope(self.vdom) { - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } + if self.cfg.force_diff || !props_are_the_same(new) { + let succeeded = scope.run_scope(self.vdom); + + if succeeded { + self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); } } @@ -540,25 +575,8 @@ impl<'bump> DiffMachine<'bump> { self.seen_scopes.insert(scope_addr); } else { - todo!(); - - // let mut old_iter = RealChildIterator::new(old_node, &self.vdom); - // let first = old_iter - // .next() - // .expect("Components should generate a placeholder root"); - - // // remove any leftovers - // for to_remove in old_iter { - // self.mutations.push_root(to_remove.direct_id()); - // self.mutations.remove(); - // } - - // // seems like we could combine this into a single instruction.... - // self.replace_node_with_node(first.direct_id(), old_node, new_node); - - // // Wipe the old one and plant the new one - // let old_scope = old.ass_scope.get().unwrap(); - // self.destroy_scopes(old_scope); + self.stack + .create_node(new_node, MountType::Replace { old: old_node }); } } @@ -573,6 +591,11 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } + fn diff_suspended_nodes(&mut self, old: &'bump VSuspended, new: &'bump VSuspended) { + new.dom_id.set(old.dom_id.get()); + self.attach_suspended_node_to_scope(new); + } + // ============================================= // Utilites for creating new diff instructions // ============================================= @@ -608,7 +631,7 @@ impl<'bump> DiffMachine<'bump> { old_anchor.dom_id.set(new_anchor.dom_id.get()); } ([VNode::Anchor(anchor)], _) => { - let el = anchor.dom_id.get().unwrap(); + let el = anchor.dom_id.get(); self.stack .create_children(new, MountType::ReplaceByElementId { el }); } @@ -647,24 +670,27 @@ impl<'bump> DiffMachine<'bump> { // 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>]) { // Handled these cases in `diff_children` before calling this function. - log::debug!("diffing non-keyed case"); debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); for (new, old) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::DiffNode { new, old }); + self.stack.push(DiffInstruction::Diff { new, old }); } - if old.len() > new.len() { - self.remove_nodes(&old[new.len()..]); - } else if new.len() > old.len() { - log::debug!("Calling create children on array differences"); - self.stack.create_children( - &new[old.len()..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); + use std::cmp::Ordering; + match old.len().cmp(&new.len()) { + Ordering::Greater => self.remove_nodes(&old[new.len()..]), + Ordering::Less => { + self.stack.create_children( + &new[old.len()..], + MountType::InsertAfter { + other_node: old.last().unwrap(), + }, + ); + } + Ordering::Equal => { + // nothing - they're the same size + } } } @@ -748,7 +774,7 @@ impl<'bump> DiffMachine<'bump> { if old.key() != new.key() { break; } - self.stack.push(DiffInstruction::DiffNode { old, new }); + self.stack.push(DiffInstruction::Diff { old, new }); left_offset += 1; } @@ -887,8 +913,8 @@ impl<'bump> DiffMachine<'bump> { stack.create_node(new_node, MountType::Absorb); } else { // this funciton should never take LIS indicies - stack.push(DiffInstruction::PrepareMoveNode { node: new_node }); - stack.push(DiffInstruction::DiffNode { + stack.push(DiffInstruction::PrepareMove { node: new_node }); + stack.push(DiffInstruction::Diff { new: new_node, old: &old[old_index], }); @@ -913,7 +939,7 @@ impl<'bump> DiffMachine<'bump> { // for each spacing, generate a mount instruction let mut lis_iter = lis_sequence.iter().rev(); let mut last = *lis_iter.next().unwrap(); - while let Some(&next) = lis_iter.next() { + for next in lis_iter { if last - next > 1 { self.stack.push_nodes_created(0); self.stack.push(DiffInstruction::Mount { @@ -925,7 +951,7 @@ impl<'bump> DiffMachine<'bump> { apply(idx + next + 1, new_node, &mut self.stack); } } - last = next; + last = *next; } // add mount instruction for the first items not covered by the lis @@ -943,7 +969,7 @@ impl<'bump> DiffMachine<'bump> { } for idx in lis_sequence.iter().rev() { - self.stack.push(DiffInstruction::DiffNode { + self.stack.push(DiffInstruction::Diff { new: &new[*idx], old: &old[new_index_to_old_index[*idx]], }); @@ -970,7 +996,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - search_node = Some(scope.root()); + search_node = Some(scope.root_node()); } } } @@ -988,7 +1014,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - search_node = Some(scope.root()); + search_node = Some(scope.root_node()); } VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), @@ -998,6 +1024,14 @@ impl<'bump> DiffMachine<'bump> { } } + fn replace_and_create_one_with_many( + &mut self, + old: &'bump VNode<'bump>, + new: &'bump [VNode<'bump>], + ) { + // + } + fn replace_and_create_many_with_one( &mut self, old: &'bump [VNode<'bump>], @@ -1019,25 +1053,27 @@ impl<'bump> DiffMachine<'bump> { for node in nodes { match node { VNode::Text(t) => { - t.dom_id.get().map(|id| { + if let Some(id) = t.dom_id.get() { self.mutations.remove(id.as_u64()); self.vdom.collect_garbage(id); - }); + } } VNode::Suspended(s) => { - s.dom_id.get().map(|id| { + if let Some(id) = s.dom_id.get() { self.mutations.remove(id.as_u64()); self.vdom.collect_garbage(id); - }); + } } VNode::Anchor(a) => { - a.dom_id.get().map(|id| { + if let Some(id) = a.dom_id.get() { self.mutations.remove(id.as_u64()); self.vdom.collect_garbage(id); - }); + } } VNode::Element(e) => { - e.dom_id.get().map(|id| self.mutations.remove(id.as_u64())); + if let Some(id) = e.dom_id.get() { + self.mutations.remove(id.as_u64()); + } } VNode::Fragment(f) => { self.remove_nodes(f.children); @@ -1046,7 +1082,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(c) => { let scope_id = c.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - let root = scope.root(); + let root = scope.root_node(); self.remove_nodes(Some(root)); } } @@ -1070,22 +1106,6 @@ impl<'bump> DiffMachine<'bump> { } } - fn create_garbage(&mut self, node: &'bump VNode<'bump>) { - match self - .stack - .current_scope() - .and_then(|id| self.vdom.get_scope(id)) - { - Some(scope) => { - let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) }; - scope.pending_garbage.borrow_mut().push(garbage); - } - None => { - log::info!("No scope to collect garbage into") - } - } - } - /// Adds a listener closure to a scope during diff. fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &Scope) { let mut queue = scope.listeners.borrow_mut(); @@ -1093,35 +1113,18 @@ impl<'bump> DiffMachine<'bump> { queue.push(long_listener as *const _) } - /// Destroy a scope and all of its descendents. - /// - /// Calling this will run the destuctors on all hooks in the tree. - /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&mut self, old_scope: ScopeId) { - let mut nodes_to_delete = vec![old_scope]; - let mut scopes_to_explore = vec![old_scope]; - - // explore the scope tree breadth first - while let Some(scope_id) = scopes_to_explore.pop() { - // If we're planning on deleting this node, then we don't need to both rendering it - self.seen_scopes.insert(scope_id); - let scope = self.vdom.get_scope(scope_id).unwrap(); - for child in scope.descendents.borrow().iter() { - // Add this node to be explored - scopes_to_explore.push(child.clone()); - - // Also add it for deletion - nodes_to_delete.push(child.clone()); - } - } - - // Delete all scopes that we found as part of this subtree - for node in nodes_to_delete { - log::debug!("Removing scope {:#?}", node); - let _scope = self.vdom.try_remove(node).unwrap(); - // do anything we need to do to delete the scope - // I think we need to run the destructors on the hooks - // TODO + fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) { + if let Some(scope) = self + .stack + .current_scope() + .and_then(|id| self.vdom.get_scope_mut(id)) + { + // safety: this lifetime is managed by the logic on scope + let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) }; + scope + .suspended_nodes + .borrow_mut() + .insert(suspended.task_id, extended as *const _); } } } diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 5c4f39646..aa9716cb6 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -3,23 +3,18 @@ use smallvec::{smallvec, SmallVec}; /// The stack instructions we use to diff and create new nodes. #[derive(Debug)] -pub enum DiffInstruction<'a> { - DiffNode { +pub(crate) enum DiffInstruction<'a> { + Diff { old: &'a VNode<'a>, new: &'a VNode<'a>, }, - DiffChildren { - old: &'a [VNode<'a>], - new: &'a [VNode<'a>], - }, - Create { node: &'a VNode<'a>, }, /// pushes the node elements onto the stack for use in mount - PrepareMoveNode { + PrepareMove { node: &'a VNode<'a>, }, @@ -35,7 +30,7 @@ pub enum MountType<'a> { Absorb, Append, Replace { old: &'a VNode<'a> }, - ReplaceByElementId { el: ElementId }, + ReplaceByElementId { el: Option }, InsertAfter { other_node: &'a VNode<'a> }, InsertBefore { other_node: &'a VNode<'a> }, } @@ -44,6 +39,7 @@ pub(crate) struct DiffStack<'bump> { instructions: Vec>, nodes_created_stack: SmallVec<[usize; 10]>, pub scope_stack: SmallVec<[ScopeId; 5]>, + pub element_id_stack: SmallVec<[ElementId; 5]>, } impl<'bump> DiffStack<'bump> { @@ -52,6 +48,7 @@ impl<'bump> DiffStack<'bump> { instructions: Vec::with_capacity(1000), nodes_created_stack: smallvec![], scope_stack: smallvec![], + element_id_stack: smallvec![], } } @@ -63,8 +60,8 @@ impl<'bump> DiffStack<'bump> { self.instructions.pop() } - pub fn pop_scope(&mut self) -> Option { - self.scope_stack.pop() + pub fn pop_off_scope(&mut self) { + self.scope_stack.pop(); } pub fn push(&mut self, instruction: DiffInstruction<'bump>) { @@ -75,7 +72,7 @@ impl<'bump> DiffStack<'bump> { self.nodes_created_stack.push(0); self.instructions.push(DiffInstruction::Mount { and }); - for child in children.into_iter().rev() { + for child in children.iter().rev() { self.instructions .push(DiffInstruction::Create { node: child }); } @@ -85,6 +82,10 @@ impl<'bump> DiffStack<'bump> { self.nodes_created_stack.push(count); } + pub fn _push_element_id(&mut self, id: ElementId) { + self.element_id_stack.push(id); + } + pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { self.nodes_created_stack.push(0); self.instructions.push(DiffInstruction::Mount { and }); @@ -100,7 +101,7 @@ impl<'bump> DiffStack<'bump> { } pub fn current_scope(&self) -> Option { - self.scope_stack.last().map(|f| f.clone()) + self.scope_stack.last().copied() } pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 119afbf35..e11127e43 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -3,16 +3,18 @@ //! //! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy. -use crate::innerlude::{ElementId, ScopeId}; - -use bumpalo::boxed::Box as BumpBox; -use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; - use crate::{ - innerlude::NodeFactory, - innerlude::{Attribute, Listener, VNode}, + innerlude::Listener, + innerlude::{ElementId, NodeFactory, ScopeId}, +}; +use bumpalo::boxed::Box as BumpBox; +use std::{ + any::Any, + cell::{Cell, RefCell}, + fmt::Debug, + ops::Deref, + rc::Rc, }; -use std::cell::Cell; #[derive(Debug)] pub struct UserEvent { @@ -74,6 +76,120 @@ impl std::fmt::Debug for SyntheticEvent { } } +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// Work that must be completed during the EventHandler phase. + /// + /// Currently this is reserved for controlled inputs. + Immediate = 3, + + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take presedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} + +pub(crate) fn event_meta(event: &UserEvent) -> (bool, EventPriority) { + use EventPriority::*; + + match event.name { + // clipboard + "copy" | "cut" | "paste" => (true, Medium), + + // Composition + "compositionend" | "compositionstart" | "compositionupdate" => (true, Low), + + // Keyboard + "keydown" | "keypress" | "keyup" => (true, High), + + // Focus + "focus" | "blur" => (true, Low), + + // Form + "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium), + + // Mouse + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mouseout" | "mouseover" | "mouseup" => (true, High), + + "mousemove" => (false, Medium), + + // Pointer + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + (true, Medium) + } + + // Selection + "select" | "touchcancel" | "touchend" => (true, Medium), + + // Touch + "touchmove" | "touchstart" => (true, Medium), + + // Wheel + "scroll" | "wheel" => (false, Medium), + + // Media + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => (true, Medium), + + // Animation + "animationstart" | "animationend" | "animationiteration" => (true, Medium), + + // Transition + "transitionend" => (true, Medium), + + // Toggle + "toggle" => (true, Medium), + + _ => (true, Low), + } +} + +pub use on::KeyCode; + pub mod on { //! This module defines the synthetic events that all Dioxus apps enable. No matter the platform, every dioxus renderer //! will implement the same events and same behavior (bubbling, cancelation, etc). @@ -125,8 +241,13 @@ pub mod on { let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; + + // ie oncopy let event_name = stringify!($name); + + // ie copy let shortname: &'static str = &event_name[2..]; + Listener { event: shortname, mounted_node: Cell::new(None), @@ -143,8 +264,10 @@ pub mod on { ClipboardEventInner(ClipboardEvent): [ /// Called when "copy" oncopy + /// oncut oncut + /// onpaste onpaste ]; @@ -152,8 +275,10 @@ pub mod on { CompositionEventInner(CompositionEvent): [ /// oncompositionend oncompositionend + /// oncompositionstart oncompositionstart + /// oncompositionupdate oncompositionupdate ]; @@ -161,8 +286,10 @@ pub mod on { KeyboardEventInner(KeyboardEvent): [ /// onkeydown onkeydown + /// onkeypress onkeypress + /// onkeyup onkeyup ]; @@ -170,6 +297,7 @@ pub mod on { FocusEventInner(FocusEvent): [ /// onfocus onfocus + /// onblur onblur ]; @@ -178,12 +306,16 @@ pub mod on { FormEventInner(FormEvent): [ /// onchange onchange - /// oninput + + /// oninput handler oninput + /// oninvalid oninvalid + /// onreset onreset + /// onsubmit onsubmit ]; @@ -254,36 +386,50 @@ pub mod on { /// ## Reference /// - https://www.w3schools.com/tags/ev_onclick.asp /// - https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event - /// onclick + /// oncontextmenu oncontextmenu + /// ondoubleclick ondoubleclick + /// ondrag ondrag + /// ondragend ondragend + /// ondragenter ondragenter + /// ondragexit ondragexit + /// ondragleave ondragleave + /// ondragover ondragover + /// ondragstart ondragstart + /// ondrop ondrop + /// onmousedown onmousedown + /// onmouseenter onmouseenter + /// onmouseleave onmouseleave + /// onmousemove onmousemove + /// onmouseout onmouseout @@ -294,6 +440,7 @@ pub mod on { /// /// Triggered when the users's mouse hovers over an element. onmouseover + /// onmouseup onmouseup ]; @@ -301,22 +448,31 @@ pub mod on { PointerEventInner(PointerEvent): [ /// pointerdown onpointerdown + /// pointermove onpointermove + /// pointerup onpointerup + /// pointercancel onpointercancel + /// gotpointercapture ongotpointercapture + /// lostpointercapture onlostpointercapture + /// pointerenter onpointerenter + /// pointerleave onpointerleave + /// pointerover onpointerover + /// pointerout onpointerout ]; @@ -329,10 +485,13 @@ pub mod on { TouchEventInner(TouchEvent): [ /// ontouchcancel ontouchcancel + /// ontouchend ontouchend + /// ontouchmove ontouchmove + /// ontouchstart ontouchstart ]; @@ -345,48 +504,70 @@ pub mod on { MediaEventInner(MediaEvent): [ ///abort onabort + ///canplay oncanplay + ///canplaythrough oncanplaythrough + ///durationchange ondurationchange + ///emptied onemptied + ///encrypted onencrypted + ///ended onended + ///error onerror + ///loadeddata onloadeddata + ///loadedmetadata onloadedmetadata + ///loadstart onloadstart + ///pause onpause + ///play onplay + ///playing onplaying + ///progress onprogress + ///ratechange onratechange + ///seeked onseeked + ///seeking onseeking + ///stalled onstalled + ///suspend onsuspend + ///timeupdate ontimeupdate + ///volumechange onvolumechange + ///waiting onwaiting ]; @@ -394,8 +575,10 @@ pub mod on { AnimationEventInner(AnimationEvent): [ /// onanimationstart onanimationstart + /// onanimationend onanimationend + /// onanimationiteration onanimationiteration ]; @@ -827,7 +1010,7 @@ pub mod on { } // get the raw code - fn raw_code(&self) -> u32 { + pub fn raw_code(&self) -> u32 { *self as u32 } } diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index ebfe5723a..2f68be3b8 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -3,45 +3,28 @@ use std::{ cell::{Cell, RefCell, UnsafeCell}, }; +/// An abstraction over internally stored data using a hook-based memory layout. +/// +/// Hooks are allocated using Boxes and then our stored references are given out. +/// +/// It's unsafe to "reset" the hooklist, but it is safe to add hooks into it. +/// +/// Todo: this could use its very own bump arena, but that might be a tad overkill +#[derive(Default)] pub(crate) struct HookList { - vals: RefCell>>>, + vals: RefCell>, Box)>>, idx: Cell, } -impl Default for HookList { - fn default() -> Self { - Self { - vals: Default::default(), - idx: Cell::new(0), - } - } -} - -struct InnerHook { - cell: UnsafeCell, -} - -impl InnerHook { - fn new(new: T) -> Self { - Self { - cell: UnsafeCell::new(new), - } - } -} - impl HookList { pub(crate) fn next(&self) -> Option<&mut T> { self.vals.borrow().get(self.idx.get()).and_then(|inn| { self.idx.set(self.idx.get() + 1); - let raw_box = unsafe { &mut *inn.cell.get() }; + let raw_box = unsafe { &mut *inn.0.get() }; raw_box.downcast_mut::() }) } - pub(crate) fn push(&self, new: T) { - self.vals.borrow_mut().push(InnerHook::new(Box::new(new))) - } - /// This resets the internal iterator count /// It's okay that we've given out each hook, but now we have the opportunity to give it out again /// Therefore, resetting is cosudered unsafe @@ -52,18 +35,31 @@ impl HookList { self.idx.set(0); } - #[inline] + pub(crate) fn push_hook(&self, new: T, cleanup: Box) { + self.vals + .borrow_mut() + .push((UnsafeCell::new(Box::new(new)), cleanup)) + } + pub(crate) fn len(&self) -> usize { self.vals.borrow().len() } - #[inline] pub(crate) fn cur_idx(&self) -> usize { self.idx.get() } - #[inline] pub(crate) fn at_end(&self) -> bool { self.cur_idx() >= self.len() } } + +// When the scope is dropped, we want to call the cleanup function for each of the hooks +impl Drop for HookList { + fn drop(&mut self) { + self.vals + .borrow_mut() + .drain(..) + .for_each(|(mut state, cleanup)| cleanup(state.get_mut())); + } +} diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 47e7fcda9..f90f42d59 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -11,83 +11,27 @@ use crate::innerlude::*; use futures_util::FutureExt; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - future::Future, - rc::Rc, -}; - -/// This hook enables the ability to expose state to children further down the VirtualDOM Tree. -/// -/// This is a hook, so it may not be called conditionally! -/// -/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`) -/// so don't put it in a conditional. -/// -/// When the component is dropped, so is the context. Be aware of this behavior when consuming -/// the context via Rc/Weak. -/// -/// -/// -pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc -where - T: 'static, - F: FnOnce() -> T, -{ - let ty = TypeId::of::(); - let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty); - - let is_initialized = cx.use_hook( - |_| false, - |s| { - let i = s.clone(); - *s = true; - i - }, - |_| {}, - ); - - match (is_initialized, contains_key) { - // Do nothing, already initialized and already exists - (true, true) => {} - - // Needs to be initialized - (false, false) => cx.add_shared_state(init()), - - _ => debug_assert!(false, "Cannot initialize two contexts of the same type"), - }; - - use_consume_state::(cx) -} - -/// There are hooks going on here! -pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc { - use_try_consume_state::(cx).unwrap() -} - -/// Uses a context, storing the cached value around -/// -/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a -/// context was added later. This allows using another hook as a fallback -/// -pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc> { - struct UseContextHook(Option>); - - cx.use_hook( - move |_| UseContextHook(cx.consume_shared_state::()), - move |hook| hook.0.as_ref(), - |_| {}, - ) -} +use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc}; /// Awaits the given task, forcing the component to re-render when the value is ready. /// +/// Returns the handle to the task and the value (if it is ready, else None). /// +/// ``` +/// static Example: FC<()> = |cx, props| { +/// let (task, value) = use_task(|| async { +/// timer::sleep(Duration::from_secs(1)).await; +/// "Hello World" +/// }); /// -/// -pub fn use_task<'src, Out, Fut, Init, P>( - cx: Context<'src, P>, +/// match contents { +/// Some(contents) => rsx!(cx, div { "{title}" }), +/// None => rsx!(cx, div { "Loading..." }), +/// } +/// }; +/// ``` +pub fn use_task<'src, Out, Fut, Init>( + cx: Context<'src>, task_initializer: Init, ) -> (&'src TaskHandle, &'src Option) where @@ -110,7 +54,7 @@ where let slot = task_dump.clone(); - let updater = cx.prepare_update(); + let updater = cx.schedule_update_any(); let originator = cx.scope.our_arena_idx; let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move { @@ -145,8 +89,8 @@ where /// # Example /// /// -pub fn use_suspense<'src, Out, Fut, Cb, P>( - cx: Context<'src, P>, +pub fn use_suspense<'src, Out, Fut, Cb>( + cx: Context<'src>, task_initializer: impl FnOnce() -> Fut, user_callback: Cb, ) -> DomTree<'src> @@ -166,11 +110,10 @@ where - if it doesn't, then we render a suspended node along with with the callback and task id */ cx.use_hook( - move |hook_idx| { + move |_| { let value = Rc::new(RefCell::new(None)); let slot = value.clone(); - - let originator = cx.scope.our_arena_idx.clone(); + let originator = cx.scope.our_arena_idx; let handle = cx.submit_task(Box::pin(task_initializer().then( move |output| async move { @@ -185,10 +128,7 @@ where Some(value) => { let out = value.downcast_ref::().unwrap(); let sus = SuspendedContext { - inner: Context { - props: &(), - scope: cx.scope, - }, + inner: Context { scope: cx.scope }, }; user_callback(sus, out) } @@ -232,10 +172,8 @@ pub(crate) struct SuspenseHook { pub value: Rc>>>, } -type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; - pub struct SuspendedContext<'a> { - pub(crate) inner: Context<'a, ()>, + pub(crate) inner: Context<'a>, } impl<'src> SuspendedContext<'src> { @@ -243,25 +181,21 @@ impl<'src> SuspendedContext<'src> { self, lazy_nodes: LazyNodes<'src, F>, ) -> DomTree<'src> { - let scope_ref = self.inner.scope; let bump = &self.inner.scope.frames.wip_frame().bump; - Some(lazy_nodes.into_vnode(NodeFactory { bump })) } } #[derive(Clone, Copy)] -pub struct NodeRef<'src, T: 'static>(&'src RefCell); +pub struct NodeRef<'src, T: 'static>(&'src RefCell>); -pub fn use_node_ref(cx: Context

) -> NodeRef { - cx.use_hook( - |f| {}, - |f| { - // - todo!() - }, - |f| { - // - }, - ) +impl<'a, T> Deref for NodeRef<'a, T> { + type Target = RefCell>; + fn deref(&self) -> &Self::Target { + self.0 + } +} + +pub fn use_node_ref(cx: Context) -> NodeRef { + cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {}) } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b588215b3..33aa21ede 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -4,7 +4,7 @@ /* Navigating this crate: - virtual_dom: the primary entrypoint for the crate -- scheduler: the core interior logic called by virtual_dom +- scheduler: the core interior logic called by the [`VirtualDom`] - nodes: the definition of VNodes, listeners, etc. - diff: the stackmachine-based diffing algorithm - hooks: foundational hooks that require crate-private APIs @@ -24,8 +24,10 @@ pub mod hooklist; pub mod hooks; pub mod mutations; pub mod nodes; +pub mod resources; pub mod scheduler; pub mod scope; +pub mod tasks; pub mod test_dom; pub mod util; pub mod virtual_dom; @@ -43,22 +45,24 @@ pub(crate) mod innerlude { pub use crate::hooks::*; pub use crate::mutations::*; pub use crate::nodes::*; + pub(crate) use crate::resources::*; pub use crate::scheduler::*; pub use crate::scope::*; + pub use crate::tasks::*; pub use crate::test_dom::*; pub use crate::util::*; pub use crate::virtual_dom::*; pub type DomTree<'a> = Option>; - pub type FC

= fn(Context

) -> DomTree; + pub type FC

= for<'a> fn(Context<'a>, &'a P) -> DomTree<'a>; pub use dioxus_core_macro::{format_args_f, html, rsx}; } pub use crate::innerlude::{ - format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId, - EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, - SuspendedContext, SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC, + format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, + LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext, + SyntheticEvent, TaskHandle, TestDom, UserEvent, VNode, VirtualDom, FC, }; pub mod prelude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 58f259c72..5346042a1 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -12,7 +12,6 @@ use std::{ cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, marker::PhantomData, - rc::Rc, }; /// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. @@ -26,6 +25,7 @@ pub enum VNode<'src> { /// /// # Example /// + /// ``` /// let node = cx.render(rsx!{ "hello" }).unwrap(); /// /// if let VNode::Text(vtext) = node { @@ -188,6 +188,8 @@ pub struct VElement<'a> { pub dom_id: Cell>, + pub parent_id: Cell>, + pub listeners: &'a [Listener<'a>], pub attributes: &'a [Attribute<'a>], @@ -195,6 +197,21 @@ pub struct VElement<'a> { pub children: &'a [VNode<'a>], } +impl Debug for VElement<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VElement") + .field("tag_name", &self.tag_name) + .field("namespace", &self.namespace) + .field("key", &self.key) + .field("dom_id", &self.dom_id) + .field("parent_id", &self.parent_id) + .field("listeners", &self.listeners.len()) + .field("attributes", &self.attributes) + .field("children", &self.children) + .finish() + } +} + /// A trait for any generic Dioxus Element. /// /// This trait provides the ability to use custom elements in the `rsx!` macro. @@ -244,13 +261,16 @@ pub struct Attribute<'a> { /// An event listener. /// IE onclick, onkeydown, etc pub struct Listener<'bump> { + /// The ID of the node that this listener is mounted to + /// Used to generate the event listener's ID on the DOM pub mounted_node: Cell>, /// The type of event to listen for. /// - /// IE "onclick" - whatever the renderer needs to attach the listener by name. + /// IE "click" - whatever the renderer needs to attach the listener by name. pub event: &'static str, + /// The actual callback that the user specified pub(crate) callback: RefCell>>, } @@ -266,7 +286,7 @@ pub struct VComponent<'src> { // Function pointer to the FC that was used to generate this component pub user_fc: *const (), - pub(crate) caller: Rc DomTree>, + pub(crate) caller: &'src dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, pub(crate) children: &'src [VNode<'src>], @@ -283,8 +303,7 @@ pub struct VComponent<'src> { pub struct VSuspended<'a> { pub task_id: u64, pub dom_id: Cell>, - pub(crate) callback: - RefCell) -> DomTree<'a>>>>, + pub callback: RefCell) -> DomTree<'a>>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -303,7 +322,7 @@ impl<'a> NodeFactory<'a> { #[inline] pub fn bump(&self) -> &'a bumpalo::Bump { - &self.bump + self.bump } pub fn render_directly(&self, lazy_nodes: LazyNodes<'a, F>) -> DomTree<'a> @@ -413,6 +432,7 @@ impl<'a> NodeFactory<'a> { attributes, children, dom_id: empty_cell(), + parent_id: empty_cell(), })) } @@ -461,12 +481,12 @@ impl<'a> NodeFactory<'a> { // - This comparator is only called on a corresponding set of bumpframes let props_memoized = unsafe { let real_other: &P = &*(other.raw_props as *const _ as *const P); - props.memoize(&real_other) + props.memoize(real_other) }; // It's only okay to memoize if there are no children and the props can be memoized // Implementing memoize is unsafe and done automatically with the props trait - match (props_memoized, children.len() == 0) { + match (props_memoized, children.is_empty()) { (true, true) => true, _ => false, } @@ -488,6 +508,8 @@ impl<'a> NodeFactory<'a> { std::mem::drop(b); has_dropped = true; + } else { + panic!("Drop props called twice - this is an internal failure of Dioxus"); } } }); @@ -497,15 +519,20 @@ impl<'a> NodeFactory<'a> { RefCell::new(Some(drop_props)) }; - let is_static = children.len() == 0 && P::IS_STATIC && key.is_none(); + let is_static = children.is_empty() && P::IS_STATIC && key.is_none(); let key = key.map(|f| self.raw_text(f).0); - let caller = NodeFactory::create_component_caller(component, raw_props); + let caller: &'a mut dyn for<'b> Fn(&'b Scope) -> DomTree<'b> = + bump.alloc(move |scope: &Scope| -> DomTree { + let props: &'_ P = unsafe { &*(raw_props as *const P) }; + let res = component(Context { scope }, props); + unsafe { std::mem::transmute(res) } + }); - let can_memoize = children.len() == 0 && P::IS_STATIC; + let can_memoize = children.is_empty() && P::IS_STATIC; - VNode::Component(bump.alloc_with(|| VComponent { + VNode::Component(bump.alloc(VComponent { user_fc, comparator, raw_props, @@ -519,24 +546,6 @@ impl<'a> NodeFactory<'a> { })) } - pub(crate) fn create_component_caller<'g, P: 'g>( - component: FC

, - raw_props: *const (), - ) -> Rc Fn(&'r Scope) -> DomTree<'r>> { - type Captured<'a> = Rc Fn(&'r Scope) -> DomTree<'r> + 'a>; - let caller: Captured = Rc::new(move |scp: &Scope| -> DomTree { - // cast back into the right lifetime - let safe_props: &'_ P = unsafe { &*(raw_props as *const P) }; - let cx: Context

= Context { - props: safe_props, - scope: scp, - }; - let res = component(cx); - unsafe { std::mem::transmute(res) } - }); - unsafe { std::mem::transmute::<_, Captured<'static>>(caller) } - } - pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> { let children = node_iter.into_vnode_list(self); @@ -602,7 +611,7 @@ where nodes.push(node.into_vnode(cx)); } - if nodes.len() == 0 { + if nodes.is_empty() { nodes.push(VNode::Anchor(VAnchor { dom_id: empty_cell(), })); @@ -722,14 +731,14 @@ where // Conveniently, we also support "null" (nothing) passed in impl IntoVNode<'_> for () { - fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, cx: NodeFactory) -> VNode { cx.fragment_from_iter(None as Option) } } // Conveniently, we also support "None" impl IntoVNode<'_> for Option<()> { - fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, cx: NodeFactory) -> VNode { cx.fragment_from_iter(None as Option) } } @@ -744,12 +753,12 @@ impl<'a> IntoVNode<'a> for Option> { } impl IntoVNode<'_> for &'static str { - fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, cx: NodeFactory) -> VNode { cx.static_text(self) } } impl IntoVNode<'_> for Arguments<'_> { - fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, cx: NodeFactory) -> VNode { cx.text(self) } } @@ -763,13 +772,12 @@ impl Debug for NodeFactory<'_> { impl Debug for VNode<'_> { fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match &self { - VNode::Element(el) => { - // - s.debug_struct("VElement") - .field("name", &el.tag_name) - .field("key", &el.key) - .finish() - } + VNode::Element(el) => s + .debug_struct("VElement") + .field("name", &el.tag_name) + .field("key", &el.key) + .finish(), + VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), VNode::Anchor(_) => write!(s, "VAnchor"), diff --git a/packages/core/src/resources.rs b/packages/core/src/resources.rs new file mode 100644 index 000000000..f0361b5f7 --- /dev/null +++ b/packages/core/src/resources.rs @@ -0,0 +1,89 @@ +use crate::innerlude::*; +use slab::Slab; + +use std::{cell::UnsafeCell, rc::Rc}; +#[derive(Clone)] +pub(crate) struct ResourcePool { + /* + This *has* to be an UnsafeCell. + + Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to + its bumpframe conents immutably. + + However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly + simple to audit. + + Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) + */ + pub components: Rc>>, + + /* + Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy + that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. + + This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. + + In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... + */ + pub raw_elements: Rc>>>, + + pub channel: EventChannel, +} + +impl ResourcePool { + /// this is unsafe because the caller needs to track which other scopes it's already using + pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { + let inner = unsafe { &*self.components.get() }; + inner.get(idx.0) + } + + /// this is unsafe because the caller needs to track which other scopes it's already using + pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { + let inner = unsafe { &mut *self.components.get() }; + inner.get_mut(idx.0) + } + + // return a bumpframe with a lifetime attached to the arena borrow + // this is useful for merging lifetimes + pub fn with_scope_vnode<'b>( + &self, + _id: ScopeId, + _f: impl FnOnce(&mut Scope) -> &VNode<'b>, + ) -> Option<&VNode<'b>> { + todo!() + } + + pub fn try_remove(&self, id: ScopeId) -> Option { + let inner = unsafe { &mut *self.components.get() }; + Some(inner.remove(id.0)) + // .try_remove(id.0) + // .ok_or_else(|| Error::FatalInternal("Scope not found")) + } + + pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId { + let els = unsafe { &mut *self.raw_elements.get() }; + let entry = els.vacant_entry(); + let key = entry.key(); + let id = ElementId(key); + let node = node as *const _; + let node = unsafe { std::mem::transmute(node) }; + entry.insert(node); + id + } + + /// return the id, freeing the space of the original node + pub fn collect_garbage(&self, id: ElementId) { + todo!("garabge collection currently WIP") + // self.raw_elements.remove(id.0); + } + + pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { + let g = unsafe { &mut *self.components.get() }; + let entry = g.vacant_entry(); + let id = ScopeId(entry.key()); + entry.insert(f(id)); + id + } + + pub fn borrow_bumpframe(&self) {} +} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index d173a81f5..dcb6acb33 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -14,7 +14,7 @@ Some essential reading: Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no -point if being "fast" if you can't also be "responsive." +point in being "fast" if you can't also be "responsive." As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms). @@ -71,33 +71,41 @@ For the rest, we defer to the rIC period and work down each queue from high to l use crate::heuristics::*; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::stream::FuturesUnordered; -use futures_util::{future::FusedFuture, pin_mut, Future, FutureExt, StreamExt}; -use fxhash::{FxHashMap, FxHashSet}; +use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt}; +use fxhash::FxHashMap; +use fxhash::FxHashSet; use indexmap::IndexSet; use slab::Slab; -use smallvec::SmallVec; use std::{ any::{Any, TypeId}, - cell::{Cell, RefCell, RefMut, UnsafeCell}, - collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}, - fmt::Display, - pin::Pin, + cell::{Cell, UnsafeCell}, + collections::{HashSet, VecDeque}, rc::Rc, }; #[derive(Clone)] -pub struct EventChannel { +pub(crate) struct EventChannel { pub task_counter: Rc>, pub sender: UnboundedSender, pub schedule_any_immediate: Rc, pub submit_task: Rc TaskHandle>, - pub get_shared_context: Rc Option>>, + pub get_shared_context: GetSharedContext, } +pub type GetSharedContext = Rc Option>>; + pub enum SchedulerMsg { - Immediate(ScopeId), + // events from the host UiEvent(UserEvent), + + // setstate + Immediate(ScopeId), + + // tasks + Task(TaskMsg), +} + +pub enum TaskMsg { SubmitTask(FiberTask, u64), ToggleTask(u64), PauseTask(u64), @@ -109,7 +117,7 @@ pub enum SchedulerMsg { /// /// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components. /// -/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs +/// In Dioxus, the scheduler provides 4 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs /// out. /// /// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between @@ -137,22 +145,25 @@ pub(crate) struct Scheduler { // In-flight futures pub async_tasks: FuturesUnordered, - // scheduler stuff - pub current_priority: EventPriority, - + // // scheduler stuff + // pub current_priority: EventPriority, pub ui_events: VecDeque, pub pending_immediates: VecDeque, pub pending_tasks: VecDeque, + pub batched_events: VecDeque, + pub garbage_scopes: HashSet, - pub lanes: [PriorityLane; 4], + pub dirty_scopes: IndexSet, + pub saved_state: Option>, + pub in_progress: bool, } impl Scheduler { - pub fn new() -> Self { + pub(crate) fn new() -> Self { /* Preallocate 2000 elements and 100 scopes to avoid dynamic allocation. Perhaps this should be configurable from some external config? @@ -173,12 +184,13 @@ impl Scheduler { Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap()) }, submit_task: { - let sender = sender.clone(); Rc::new(move |fiber_task| { let task_id = task_counter.get(); task_counter.set(task_id + 1); sender - .unbounded_send(SchedulerMsg::SubmitTask(fiber_task, task_id)) + .unbounded_send(SchedulerMsg::Task(TaskMsg::SubmitTask( + fiber_task, task_id, + ))) .unwrap(); TaskHandle { our_id: task_id, @@ -204,155 +216,217 @@ impl Scheduler { }; let pool = ResourcePool { - components: components.clone(), + components, raw_elements, channel, }; + let async_tasks = FuturesUnordered::new(); + + // push a task that would never resolve - prevents us from immediately aborting the scheduler + async_tasks.push(Box::pin(async { + std::future::pending::<()>().await; + ScopeId(0) + }) as FiberTask); + + let saved_state = SavedDiffWork { + mutations: Mutations::new(), + stack: DiffStack::new(), + seen_scopes: Default::default(), + }; + Self { pool, + receiver, - async_tasks: FuturesUnordered::new(), + async_tasks, pending_garbage: FxHashSet::default(), heuristics, - // a storage for our receiver to dump into ui_events: VecDeque::new(), pending_immediates: VecDeque::new(), pending_tasks: VecDeque::new(), + batched_events: VecDeque::new(), + garbage_scopes: HashSet::new(), - current_priority: EventPriority::Low, - - // a dedicated fiber for each priority - lanes: [ - PriorityLane::new(), - PriorityLane::new(), - PriorityLane::new(), - PriorityLane::new(), - ], + dirty_scopes: Default::default(), + saved_state: Some(saved_state), + in_progress: false, } } - pub fn manually_poll_events(&mut self) { - while let Ok(Some(msg)) = self.receiver.try_next() { - self.handle_channel_msg(msg); - } - } + // returns true if the event is discrete + pub fn handle_ui_event(&mut self, event: UserEvent) -> bool { + let (discrete, priority) = event_meta(&event); - // Converts UI events into dirty scopes with various priorities - pub fn consume_pending_events(&mut self) { - while let Some(trigger) = self.ui_events.pop_back() { - if let Some(scope) = self.pool.get_scope_mut(trigger.scope) { - if let Some(element) = trigger.mounted_dom_id { - let priority = match &trigger.event { - SyntheticEvent::ClipboardEvent(_) => {} - SyntheticEvent::CompositionEvent(_) => {} - SyntheticEvent::KeyboardEvent(_) => {} - SyntheticEvent::FocusEvent(_) => {} - SyntheticEvent::FormEvent(_) => {} - SyntheticEvent::SelectionEvent(_) => {} - SyntheticEvent::TouchEvent(_) => {} - SyntheticEvent::WheelEvent(_) => {} - SyntheticEvent::MediaEvent(_) => {} - SyntheticEvent::AnimationEvent(_) => {} - SyntheticEvent::TransitionEvent(_) => {} - SyntheticEvent::ToggleEvent(_) => {} - SyntheticEvent::MouseEvent(_) => {} - SyntheticEvent::PointerEvent(_) => {} - SyntheticEvent::GenericEvent(_) => {} - }; + if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(element) = event.mounted_dom_id { + // TODO: bubble properly here + scope.call_listener(event, element); - scope.call_listener(trigger.event, element); - // let receiver = self.immediate_receiver.clone(); - // let mut receiver = receiver.borrow_mut(); - - // // Drain the immediates into the dirty scopes, setting the appropiate priorities - // while let Ok(Some(dirty_scope)) = receiver.try_next() { + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + // // self.add_dirty_scope(dirty_scope, trigger.priority) - // } } } } + + // use EventPriority::*; + + // match priority { + // Immediate => todo!(), + // High => todo!(), + // Medium => todo!(), + // Low => todo!(), + // } + + discrete + } + + fn prepare_work(&mut self) { + // while let Some(trigger) = self.ui_events.pop_back() { + // if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {} + // } } // nothing to do, no events on channels, no work pub fn has_any_work(&self) -> bool { - let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some(); - pending_lanes || self.has_pending_events() - } - - pub fn has_pending_events(&self) -> bool { - self.ui_events.len() > 0 - } - - fn shift_priorities(&mut self) { - self.current_priority = match ( - self.lanes[0].has_work(), - self.lanes[1].has_work(), - self.lanes[2].has_work(), - self.lanes[3].has_work(), - ) { - (true, _, _, _) => EventPriority::Immediate, - (false, true, _, _) => EventPriority::High, - (false, false, true, _) => EventPriority::Medium, - (false, false, false, _) => EventPriority::Low, - }; + !(self.dirty_scopes.is_empty() && self.ui_events.is_empty()) } /// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work fn balance_lanes(&mut self) {} - fn load_current_lane(&mut self) -> &mut PriorityLane { - match self.current_priority { - EventPriority::Immediate => todo!(), - EventPriority::High => todo!(), - EventPriority::Medium => todo!(), - EventPriority::Low => todo!(), - } - } - fn save_work(&mut self, lane: SavedDiffWork) { let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) }; - self.load_current_lane().saved_state = Some(saved); + self.saved_state = Some(saved); } - fn load_work(&mut self) -> SavedDiffWork<'static> { - match self.current_priority { - EventPriority::Immediate => todo!(), - EventPriority::High => todo!(), - EventPriority::Medium => todo!(), - EventPriority::Low => todo!(), + unsafe fn load_work(&mut self) -> SavedDiffWork<'static> { + self.saved_state.take().unwrap().extend() + } + + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + match msg { + // + SchedulerMsg::Task(msg) => todo!(), + + SchedulerMsg::Immediate(_) => todo!(), + + SchedulerMsg::UiEvent(event) => { + // + + let (discrete, priority) = event_meta(&event); + + if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(element) = event.mounted_dom_id { + // TODO: bubble properly here + scope.call_listener(event, element); + + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + // + // self.add_dirty_scope(dirty_scope, trigger.priority) + } + } + } + + discrete; + } } } - /// Work the scheduler down, not polling any ongoing tasks. + /// Load the current lane, and work on it, periodically checking in if the deadline has been reached. /// - /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. - pub fn work_sync<'a>(&'a mut self) -> Vec> { - let mut committed_mutations = Vec::new(); + /// Returns true if the lane is finished before the deadline could be met. + pub fn work_on_current_lane( + &mut self, + deadline_reached: impl FnMut() -> bool, + mutations: &mut Vec, + ) -> bool { + // Work through the current subtree, and commit the results when it finishes + // When the deadline expires, give back the work + let saved_state = unsafe { self.load_work() }; - self.manually_poll_events(); + // We have to split away some parts of ourself - current lane is borrowed mutably + let shared = self.pool.clone(); + let mut machine = unsafe { saved_state.promote(&shared) }; - if !self.has_any_work() { - self.clean_up_garbage(); - return committed_mutations; + let mut ran_scopes = FxHashSet::default(); + + if machine.stack.is_empty() { + let shared = self.pool.clone(); + + self.dirty_scopes.sort_by(|a, b| { + let h1 = shared.get_scope(*a).unwrap().height; + let h2 = shared.get_scope(*b).unwrap().height; + h1.cmp(&h2) + }); + + if let Some(scopeid) = self.dirty_scopes.pop() { + log::info!("handlng dirty scope {:#?}", scopeid); + if !ran_scopes.contains(&scopeid) { + ran_scopes.insert(scopeid); + + let mut component = self.pool.get_scope_mut(scopeid).unwrap(); + if component.run_scope(&self.pool) { + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + machine.stack.scope_stack.push(scopeid); + machine.stack.push(DiffInstruction::Diff { new, old }); + } + } + } } - self.consume_pending_events(); + let work_completed = machine.work(deadline_reached); - while self.has_any_work() { - self.shift_priorities(); - self.work_on_current_lane(&mut || false, &mut committed_mutations); + log::debug!("Working finished? {:?}", work_completed); + + // log::debug!("raw edits {:?}", machine.mutations.edits); + + let mut machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; + // let mut saved = machine.save(); + + if work_completed { + for node in machine.seen_scopes.drain() { + // self.dirty_scopes.clear(); + // self.ui_events.clear(); + self.dirty_scopes.remove(&node); + // self.dirty_scopes.remove(&node); + } + + let mut new_mutations = Mutations::new(); + + for edit in machine.mutations.edits.drain(..) { + new_mutations.edits.push(edit); + } + + // for edit in saved.edits.drain(..) { + // new_mutations.edits.push(edit); + // } + + // std::mem::swap(&mut new_mutations, &mut saved.mutations); + + mutations.push(new_mutations); + + // log::debug!("saved edits {:?}", mutations); + + let mut saved = machine.save(); + self.save_work(saved); + true + + // self.save_work(saved); + // false + } else { + false } - - committed_mutations } /// The primary workhorse of the VirtualDOM. @@ -360,9 +434,9 @@ impl Scheduler { /// Uses some fairly complex logic to schedule what work should be produced. /// /// Returns a list of successful mutations. - pub async fn work_with_deadline<'a>( + pub fn work_with_deadline<'a>( &'a mut self, - mut deadline_reached: Pin>>, + mut deadline: impl FnMut() -> bool, ) -> Vec> { /* Strategy: @@ -378,6 +452,14 @@ impl Scheduler { - If there are no pending discrete events, then check for continuous events. These can be completely batched + - we batch completely until we run into a discrete event + - all continuous events are batched together + - so D C C C C C would be two separate events - D and C. IE onclick and onscroll + - D C C C C C C D C C C D would be D C D C D in 5 distinct phases. + + - !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future + - we need to keep track of element parents to be able to traverse properly + Open questions: - what if we get two clicks from the component during the same slice? @@ -387,427 +469,107 @@ impl Scheduler { */ let mut committed_mutations = Vec::>::new(); - loop { - // Internalize any pending work since the last time we ran - self.manually_poll_events(); + while self.has_any_work() { + // switch our priority, pop off any work + while let Some(event) = self.ui_events.pop_front() { + if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(element) = event.mounted_dom_id { + log::info!("Calling listener {:?}, {:?}", event.scope, element); - // Wait for any new events if we have nothing to do - if !self.has_any_work() { - self.clean_up_garbage(); - let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await; + // TODO: bubble properly here + scope.call_listener(event, element); - if deadline_expired { - return committed_mutations; + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + match dirty_scope { + SchedulerMsg::Immediate(im) => { + self.dirty_scopes.insert(im); + } + SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e), + SchedulerMsg::Task(_) => todo!(), + } + } + } } } - // Create work from the pending event queue - self.consume_pending_events(); + let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations); - // shift to the correct lane - self.shift_priorities(); - - let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some(); - - let finished_before_deadline = - self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations); - - if !finished_before_deadline { - break; + if !work_complete { + return committed_mutations; } } committed_mutations } - /// Load the current lane, and work on it, periodically checking in if the deadline has been reached. + /// Work the scheduler down, not polling any ongoing tasks. /// - /// Returns true if the lane is finished before the deadline could be met. - pub fn work_on_current_lane( - &mut self, - deadline_reached: &mut impl FnMut() -> bool, - mutations: &mut Vec, - ) -> bool { - // Work through the current subtree, and commit the results when it finishes - // When the deadline expires, give back the work - let saved_state = self.load_work(); + /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor. + pub fn work_sync<'a>(&'a mut self) -> Vec> { + let mut committed_mutations = Vec::new(); - // We have to split away some parts of ourself - current lane is borrowed mutably + while let Ok(Some(msg)) = self.receiver.try_next() { + self.handle_channel_msg(msg); + } + + if !self.has_any_work() { + return committed_mutations; + } + + while self.has_any_work() { + self.prepare_work(); + self.work_on_current_lane(|| false, &mut committed_mutations); + } + + committed_mutations + } + + /// Restart the entire VirtualDOM from scratch, wiping away any old state and components. + /// + /// Typically used to kickstart the VirtualDOM after initialization. + pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations { let mut shared = self.pool.clone(); - let mut machine = unsafe { saved_state.promote(&mut shared) }; + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); - if machine.stack.is_empty() { - let shared = self.pool.clone(); + // TODO: drain any in-flight work + let cur_component = self + .pool + .get_scope_mut(base_scope) + .expect("The base scope should never be moved"); - self.current_lane().dirty_scopes.sort_by(|a, b| { - let h1 = shared.get_scope(*a).unwrap().height; - let h2 = shared.get_scope(*b).unwrap().height; - h1.cmp(&h2) - }); + // We run the component. If it succeeds, then we can diff it and add the changes to the dom. + if cur_component.run_scope(&self.pool) { + diff_machine + .stack + .create_node(cur_component.frames.fin_head(), MountType::Append); - if let Some(scope) = self.current_lane().dirty_scopes.pop() { - let component = self.pool.get_scope(scope).unwrap(); - let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); - machine.stack.push(DiffInstruction::DiffNode { new, old }); - } - } + diff_machine.stack.scope_stack.push(base_scope); - let deadline_expired = machine.work(deadline_reached); - - let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) }; - let mut saved = machine.save(); - - if deadline_expired { - self.save_work(saved); - false + diff_machine.work(|| false); } else { - for node in saved.seen_scopes.drain() { - self.current_lane().dirty_scopes.remove(&node); - } - - let mut new_mutations = Mutations::new(); - std::mem::swap(&mut new_mutations, &mut saved.mutations); - - mutations.push(new_mutations); - self.save_work(saved); - true - } - } - - // waits for a trigger, canceling early if the deadline is reached - // returns true if the deadline was reached - // does not return the trigger, but caches it in the scheduler - pub async fn wait_for_any_trigger( - &mut self, - deadline: &mut Pin>>, - ) -> bool { - use futures_util::future::{select, Either}; - - let event_fut = async { - match select(self.receiver.next(), self.async_tasks.next()).await { - Either::Left((msg, _other)) => { - self.handle_channel_msg(msg.unwrap()); - } - Either::Right((task, _other)) => { - // do nothing, async task will likely generate a set of scheduler messages - } - } - }; - - pin_mut!(event_fut); - - match select(event_fut, deadline).await { - Either::Left((msg, _other)) => false, - Either::Right((deadline, _)) => true, - } - } - - pub fn current_lane(&mut self) -> &mut PriorityLane { - match self.current_priority { - EventPriority::Immediate => &mut self.lanes[0], - EventPriority::High => &mut self.lanes[1], - EventPriority::Medium => &mut self.lanes[2], - EventPriority::Low => &mut self.lanes[3], - } - } - - pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { - match msg { - SchedulerMsg::Immediate(_) => todo!(), - SchedulerMsg::UiEvent(_) => todo!(), - - // - SchedulerMsg::SubmitTask(_, _) => todo!(), - SchedulerMsg::ToggleTask(_) => todo!(), - SchedulerMsg::PauseTask(_) => todo!(), - SchedulerMsg::ResumeTask(_) => todo!(), - SchedulerMsg::DropTask(_) => todo!(), - } - } - - fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { - todo!() - // match priority { - // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope), - // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope), - // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), - // }; - } - - fn collect_garbage(&mut self, id: ElementId) { - // - } - - pub fn clean_up_garbage(&mut self) { - let mut scopes_to_kill = Vec::new(); - let mut garbage_list = Vec::new(); - - for scope in self.garbage_scopes.drain() { - let scope = self.pool.get_scope_mut(scope).unwrap(); - for node in scope.consume_garbage() { - garbage_list.push(node); - } - - while let Some(node) = garbage_list.pop() { - match &node { - VNode::Text(_) => { - self.pool.collect_garbage(node.mounted_id()); - } - VNode::Anchor(_) => { - self.pool.collect_garbage(node.mounted_id()); - } - VNode::Suspended(_) => { - self.pool.collect_garbage(node.mounted_id()); - } - - VNode::Element(el) => { - self.pool.collect_garbage(node.mounted_id()); - for child in el.children { - garbage_list.push(child); - } - } - - VNode::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - - VNode::Component(comp) => { - // TODO: run the hook destructors and then even delete the scope - - let scope_id = comp.associated_scope.get().unwrap(); - let scope = self.pool.get_scope(scope_id).unwrap(); - let root = scope.root(); - - garbage_list.push(root); - scopes_to_kill.push(scope_id); - } - } - } + // todo: should this be a hard error? + log::warn!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); } - for scope in scopes_to_kill.drain(..) { - // - // kill em + unsafe { std::mem::transmute(diff_machine.mutations) } + } + + pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations { + let cur_component = self + .pool + .get_scope_mut(base_scope) + .expect("The base scope should never be moved"); + + if cur_component.run_scope(&self.pool) { + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self.pool); + diff_machine.cfg.force_diff = true; + diff_machine.diff_scope(base_scope); + diff_machine.mutations + } else { + Mutations::new() } } } - -pub(crate) struct PriorityLane { - pub dirty_scopes: IndexSet, - pub saved_state: Option>, - pub in_progress: bool, -} - -impl PriorityLane { - pub fn new() -> Self { - Self { - saved_state: None, - dirty_scopes: Default::default(), - in_progress: false, - } - } - - fn has_work(&self) -> bool { - todo!() - } - - fn work(&mut self) { - let scope = self.dirty_scopes.pop(); - } -} - -pub struct TaskHandle { - pub sender: UnboundedSender, - pub our_id: u64, -} - -impl TaskHandle { - /// Toggles this coroutine off/on. - /// - /// This method is not synchronous - your task will not stop immediately. - pub fn toggle(&self) { - self.sender - .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn resume(&self) { - self.sender - .unbounded_send(SchedulerMsg::ResumeTask(self.our_id)) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn stop(&self) { - self.sender - .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn restart(&self) { - self.sender - .unbounded_send(SchedulerMsg::ToggleTask(self.our_id)) - .unwrap() - } -} - -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ScopeId(pub usize); - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ElementId(pub usize); -impl Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl ElementId { - pub fn as_u64(self) -> u64 { - self.0 as u64 - } -} - -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - NoPriority = 0 -/// - LowPriority = 1 -/// - NormalPriority = 2 -/// - UserBlocking = 3 -/// - HighPriority = 4 -/// - ImmediatePriority = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take presedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -#[derive(Clone)] -pub(crate) struct ResourcePool { - /* - This *has* to be an UnsafeCell. - - Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to - its bumpframe conents immutably. - - However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly - simple to audit. - - Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) - */ - pub components: Rc>>, - - /* - Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy - that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. - - This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. - - In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... - */ - pub raw_elements: Rc>>, - - pub channel: EventChannel, -} - -impl ResourcePool { - /// this is unsafe because the caller needs to track which other scopes it's already using - pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> { - let inner = unsafe { &*self.components.get() }; - inner.get(idx.0) - } - - /// this is unsafe because the caller needs to track which other scopes it's already using - pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> { - let inner = unsafe { &mut *self.components.get() }; - inner.get_mut(idx.0) - } - - pub fn with_scope<'b, O: 'static>( - &'b self, - _id: ScopeId, - _f: impl FnOnce(&'b mut Scope) -> O, - ) -> Option { - todo!() - } - - // return a bumpframe with a lifetime attached to the arena borrow - // this is useful for merging lifetimes - pub fn with_scope_vnode<'b>( - &self, - _id: ScopeId, - _f: impl FnOnce(&mut Scope) -> &VNode<'b>, - ) -> Option<&VNode<'b>> { - todo!() - } - - pub fn try_remove(&self, id: ScopeId) -> Option { - let inner = unsafe { &mut *self.components.get() }; - Some(inner.remove(id.0)) - // .try_remove(id.0) - // .ok_or_else(|| Error::FatalInternal("Scope not found")) - } - - pub fn reserve_node(&self) -> ElementId { - let els = unsafe { &mut *self.raw_elements.get() }; - ElementId(els.insert(())) - } - - /// return the id, freeing the space of the original node - pub fn collect_garbage(&self, id: ElementId) { - todo!("garabge collection currently WIP") - // self.raw_elements.remove(id.0); - } - - pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { - let g = unsafe { &mut *self.components.get() }; - let entry = g.vacant_entry(); - let id = ScopeId(entry.key()); - entry.insert(f(id)); - id - } - - pub fn borrow_bumpframe(&self) {} -} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 0f5cae2c7..19006042e 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,5 @@ use crate::innerlude::*; -use fxhash::FxHashSet; +use fxhash::FxHashMap; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -23,37 +23,116 @@ pub struct Scope { pub(crate) parent_idx: Option, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, - pub(crate) descendents: RefCell>, // Nodes - // an internal, highly efficient storage of vnodes - // lots of safety condsiderations pub(crate) frames: ActiveFrame, - pub(crate) caller: Rc, + pub(crate) caller: *const dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, pub(crate) child_nodes: ScopeChildren<'static>, - pub(crate) pending_garbage: RefCell>>, - // Listeners + /* + we care about: + - listeners (and how to call them when an event is triggered) + - borrowed props (and how to drop them when the parent is dropped) + - suspended nodes (and how to call their callback when their associated tasks are complete) + */ pub(crate) listeners: RefCell>>, pub(crate) borrowed_props: RefCell>>, - pub(crate) suspended_nodes: RefCell>>, + pub(crate) suspended_nodes: RefCell>>, // State pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - pub(crate) memoized_updater: Rc, + // whenever set_state is called, we fire off a message to the scheduler + // this closure _is_ the method called by schedule_update that marks this component as dirty + pub(crate) memoized_updater: Rc, pub(crate) shared: EventChannel, } -// The type of closure that wraps calling components -pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>; +/// Public interface for Scopes. +impl Scope { + /// Get the root VNode for this Scope. + /// + /// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment. + /// + /// # Example + /// ```rust + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// if let VNode::VElement(node) = base.root_node() { + /// assert_eq!(node.tag_name, "div"); + /// } + /// ``` + pub fn root_node(&self) -> &VNode { + self.frames.fin_head() + } + /// Get the height of this Scope - IE the number of scopes above it. + /// + /// A Scope with a height of `0` is the root scope - there are no other scopes above it. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.height(), 0); + /// ``` + pub fn height(&self) -> u32 { + self.height + } + + /// Get the Parent of this Scope within this Dioxus VirtualDOM. + /// + /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted. + /// + /// The base component will not have a parent, and will return `None`. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.parent(), None); + /// ``` + pub fn parent(&self) -> Option { + self.parent_idx + } + + /// Get the ID of this Scope within this Dioxus VirtualDOM. + /// + /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.scope_id(), 0); + /// ``` + pub fn scope_id(&self) -> ScopeId { + self.our_arena_idx + } +} + +// The type of closure that wraps calling components /// The type of task that gets sent to the task scheduler /// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes pub type FiberTask = Pin>>; +/// Private interface for Scopes. impl Scope { // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) // we are going to break this lifetime by force in order to save it on ourselves. @@ -62,78 +141,57 @@ impl Scope { // // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom - pub(crate) fn new<'creator_node>( - caller: Rc, - arena_idx: ScopeId, - parent: Option, + pub(crate) fn new( + caller: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, + our_arena_idx: ScopeId, + parent_idx: Option, height: u32, child_nodes: ScopeChildren, shared: EventChannel, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; - let up = shared.schedule_any_immediate.clone(); - let memoized_updater = Rc::new(move || up(arena_idx)); + let schedule_any_update = shared.schedule_any_immediate.clone(); + + let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); + + let caller = caller as *const _; + + // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph + let caller = unsafe { std::mem::transmute(caller) }; Self { memoized_updater, shared, child_nodes, caller, - parent_idx: parent, - our_arena_idx: arena_idx, + parent_idx, + our_arena_idx, height, + frames: ActiveFrame::new(), hooks: Default::default(), suspended_nodes: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), borrowed_props: Default::default(), - descendents: Default::default(), - pending_garbage: Default::default(), } } pub(crate) fn update_scope_dependencies<'creator_node>( &mut self, - caller: Rc, + caller: &'creator_node dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, child_nodes: ScopeChildren, ) { - self.caller = caller; + let caller = caller as *const _; + self.caller = unsafe { std::mem::transmute(caller) }; + let child_nodes = unsafe { child_nodes.extend_lifetime() }; self.child_nodes = child_nodes; } - /// Returns true if the scope completed successfully - /// - pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { - // 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 - - self.ensure_drop_safety(pool); - - // 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 - unsafe { self.hooks.reset() }; - - // Safety: - // - We've dropped all references to the wip bump frame - unsafe { self.frames.reset_wip_frame() }; - - // Cast the caller ptr from static to one with our own reference - let render: &WrappedCaller = self.caller.as_ref(); - - match render(self) { - None => false, - Some(new_head) => { - // the user's component succeeded. We can safely cycle to the next frame - self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; - self.frames.cycle_frame(); - true - } - } + pub(crate) fn child_nodes(&self) -> ScopeChildren { + unsafe { self.child_nodes.shorten_lifetime() } } /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from @@ -145,32 +203,30 @@ impl Scope { /// /// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log /// all listeners and borrowed props so we can clear them here. + /// + /// 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(&mut self, pool: &ResourcePool) { - // make sure all garabge is collected before trying to proceed with anything else - debug_assert!( - self.pending_garbage.borrow().is_empty(), - "clean up your garabge please" - ); - - // todo!("arch changes"); - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Referrence) // right now, we don't drop - // let vdom = &self.vdom; self.borrowed_props .get_mut() .drain(..) .map(|li| unsafe { &*li }) .for_each(|comp| { // First drop the component's undropped references - let scope_id = comp.associated_scope.get().unwrap(); - let scope = pool.get_scope_mut(scope_id).unwrap(); - scope.ensure_drop_safety(pool); + let scope_id = comp + .associated_scope + .get() + .expect("VComponents should be associated with a valid Scope"); - // Now, drop our own reference - let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); - dropper(); + if let Some(scope) = pool.get_scope_mut(scope_id) { + scope.ensure_drop_safety(pool); + + let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); + drop_props(); + } }); // Now that all the references are gone, we can safely drop our own references in our listeners. @@ -178,25 +234,20 @@ impl Scope { .get_mut() .drain(..) .map(|li| unsafe { &*li }) - .for_each(|listener| { - listener.callback.borrow_mut().take(); - }); + .for_each(|listener| drop(listener.callback.borrow_mut().take())); } - // A safe wrapper around calling listeners - // - // - pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) { + /// A safe wrapper around calling listeners + pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { let listners = self.listeners.borrow_mut(); let raw_listener = listners.iter().find(|lis| { let search = unsafe { &***lis }; - let search_id = search.mounted_node.get(); - - // this assumes the node might not be mounted - should we assume that though? - match search_id.map(|f| f == element) { - Some(same) => same, - None => false, + if search.event == event.name { + let search_id = search.mounted_node.get(); + search_id.map(|f| f == element).unwrap_or(false) + } else { + false } }); @@ -204,51 +255,179 @@ impl Scope { let listener = unsafe { &**raw_listener }; let mut cb = listener.callback.borrow_mut(); if let Some(cb) = cb.as_mut() { - (cb)(event); + (cb)(event.event); } } else { log::warn!("An event was triggered but there was no listener to handle it"); } } - pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren { - unsafe { self.child_nodes.shorten_lifetime() } - } + /* + General strategy here is to load up the appropriate suspended task and then run it. + Suspended nodes cannot be called repeatedly. + */ + pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { + let mut nodes = self.suspended_nodes.borrow_mut(); - pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) { - let g = self.suspended_nodes.borrow_mut(); - - if let Some(suspended) = g.get(&task) { - let sus: &'a VSuspended<'static> = unsafe { &**suspended }; + if let Some(suspended) = nodes.remove(&task_id) { + let sus: &'a VSuspended<'static> = unsafe { &*suspended }; let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; - let bump = self.frames.wip_frame(); - let mut cb = sus.callback.borrow_mut(); - let mut _cb = cb.take().unwrap(); let cx: SuspendedContext<'a> = SuspendedContext { - inner: Context { - props: &(), - scope: &self, - }, + inner: Context { scope: self }, }; - let n: DomTree<'a> = (_cb)(cx); + + let mut cb = sus.callback.borrow_mut().take().unwrap(); + + let new_node: DomTree<'a> = (cb)(cx); } } - pub(crate) fn consume_garbage(&self) -> Vec<&VNode> { - self.pending_garbage - .borrow_mut() - .drain(..) - .map(|node| { - // safety: scopes cannot cycle without their garbage being collected. these nodes are safe - let node: &VNode<'static> = unsafe { &*node }; - let node: &VNode = unsafe { std::mem::transmute(node) }; - node - }) - .collect::>() - } + /// Render this component. + /// + /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated). + pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { + // 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 + self.ensure_drop_safety(pool); - pub fn root(&self) -> &VNode { - self.frames.fin_head() + // 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 + unsafe { self.hooks.reset() }; + + // Safety: + // - We've dropped all references to the wip bump frame + unsafe { self.frames.reset_wip_frame() }; + + // just forget about our suspended nodes while we're at it + self.suspended_nodes.get_mut().clear(); + + // guarantee that we haven't screwed up - there should be no latent references anywhere + debug_assert!(self.listeners.borrow().is_empty()); + debug_assert!(self.suspended_nodes.borrow().is_empty()); + debug_assert!(self.borrowed_props.borrow().is_empty()); + + // Cast the caller ptr from static to one with our own reference + let render: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b> = unsafe { &*self.caller }; + + // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. + if let Some(new_head) = render(self) { + // the user's component succeeded. We can safely cycle to the next frame + self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; + self.frames.cycle_frame(); + + true + } else { + false + } + } +} + +/// render the scope to a string using the rsx! syntax +pub(crate) struct ScopeRenderer<'a> { + pub skip_components: bool, + pub show_fragments: bool, + pub _scope: &'a Scope, + pub _pre_render: bool, + pub _newline: bool, + pub _indent: bool, + pub _max_depth: usize, +} + +// this is more or less a debug tool, but it'll render the entire tree to the terminal +impl<'a> ScopeRenderer<'a> { + pub fn render( + &self, + vdom: &VirtualDom, + node: &VNode, + f: &mut std::fmt::Formatter, + il: u16, + ) -> std::fmt::Result { + const INDENT: &str = " "; + let write_indent = |_f: &mut std::fmt::Formatter, le| { + for _ in 0..le { + write!(_f, "{}", INDENT).unwrap(); + } + }; + + match &node { + VNode::Text(text) => { + write_indent(f, il); + writeln!(f, "\"{}\"", text.text)? + } + VNode::Anchor(_anchor) => { + write_indent(f, il); + writeln!(f, "Anchor {{}}")?; + } + VNode::Element(el) => { + write_indent(f, il); + write!(f, "{} {{\n", el.tag_name)?; + // write!(f, "element: {}", el.tag_name)?; + let mut attr_iter = el.attributes.iter().peekable(); + + while let Some(attr) = attr_iter.next() { + match attr.namespace { + None => { + // + write_indent(f, il + 1); + writeln!(f, "{}: \"{}\"", attr.name, attr.value)? + } + + Some(ns) => { + // write the opening tag + write_indent(f, il + 1); + write!(f, " {}:\"", ns)?; + let mut cur_ns_el = attr; + 'ns_parse: loop { + write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?; + match attr_iter.peek() { + Some(next_attr) if next_attr.namespace == Some(ns) => { + cur_ns_el = attr_iter.next().unwrap(); + } + _ => break 'ns_parse, + } + } + // write the closing tag + write!(f, "\"")?; + } + } + } + + for child in el.children { + self.render(vdom, child, f, il + 1)?; + } + write_indent(f, il); + + writeln!(f, "}}")?; + } + VNode::Fragment(frag) => { + if self.show_fragments { + write_indent(f, il); + writeln!(f, "Fragment {{")?; + for child in frag.children { + self.render(vdom, child, f, il + 1)?; + } + write_indent(f, il); + writeln!(f, "}}")?; + } else { + for child in frag.children { + self.render(vdom, child, f, il)?; + } + } + } + VNode::Component(vcomp) => { + let idx = vcomp.associated_scope.get().unwrap(); + if !self.skip_components { + let new_node = vdom.get_scope(idx).unwrap().root_node(); + self.render(vdom, new_node, f, il)?; + } + } + VNode::Suspended { .. } => { + // we can't do anything with suspended nodes + } + } + Ok(()) } } diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs new file mode 100644 index 000000000..95540d153 --- /dev/null +++ b/packages/core/src/tasks.rs @@ -0,0 +1,39 @@ +use crate::innerlude::*; +use futures_channel::mpsc::UnboundedSender; + +pub struct TaskHandle { + pub(crate) sender: UnboundedSender, + pub(crate) our_id: u64, +} + +impl TaskHandle { + /// Toggles this coroutine off/on. + /// + /// This method is not synchronous - your task will not stop immediately. + pub fn toggle(&self) { + self.sender + .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) + .unwrap() + } + + /// This method is not synchronous - your task will not stop immediately. + pub fn resume(&self) { + self.sender + .unbounded_send(SchedulerMsg::Task(TaskMsg::ResumeTask(self.our_id))) + .unwrap() + } + + /// This method is not synchronous - your task will not stop immediately. + pub fn stop(&self) { + self.sender + .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) + .unwrap() + } + + /// This method is not synchronous - your task will not stop immediately. + pub fn restart(&self) { + self.sender + .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) + .unwrap() + } +} diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs index 39b9e5e74..60d2de9b8 100644 --- a/packages/core/src/test_dom.rs +++ b/packages/core/src/test_dom.rs @@ -12,25 +12,33 @@ pub struct TestDom { impl TestDom { pub fn new() -> TestDom { let bump = Bump::new(); - let mut scheduler = Scheduler::new(); + let scheduler = Scheduler::new(); TestDom { bump, scheduler } } - pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> { + pub fn new_factory(&self) -> NodeFactory { NodeFactory::new(&self.bump) } - pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a> + pub fn render_direct<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a> where F: FnOnce(NodeFactory<'a>) -> VNode<'a>, { lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) } + pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> &'a VNode<'a> + where + F: FnOnce(NodeFactory<'a>) -> VNode<'a>, + { + self.bump + .alloc(lazy_nodes.into_vnode(NodeFactory::new(&self.bump))) + } + pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mutations = Mutations::new(); let mut machine = DiffMachine::new(mutations, &self.scheduler.pool); - machine.stack.push(DiffInstruction::DiffNode { new, old }); + machine.stack.push(DiffInstruction::Diff { new, old }); machine.mutations } @@ -38,7 +46,7 @@ impl TestDom { where F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, { - let old = self.bump.alloc(self.render(left)); + let old = self.bump.alloc(self.render_direct(left)); let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); @@ -58,20 +66,18 @@ impl TestDom { F1: FnOnce(NodeFactory<'a>) -> VNode<'a>, F2: FnOnce(NodeFactory<'a>) -> VNode<'a>, { - let old = self.bump.alloc(self.render(left)); - - let new = self.bump.alloc(self.render(right)); + let (old, new) = (self.render(left), self.render(right)); let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); machine.stack.create_node(old, MountType::Append); - machine.work(&mut || false); + machine.work(|| false); let create_edits = machine.mutations; let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); - machine.stack.push(DiffInstruction::DiffNode { old, new }); + machine.stack.push(DiffInstruction::Diff { old, new }); machine.work(&mut || false); @@ -80,3 +86,15 @@ impl TestDom { (create_edits, edits) } } + +impl Default for TestDom { + fn default() -> Self { + Self::new() + } +} + +impl VirtualDom { + pub fn simulate(&mut self) { + // + } +} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index d2df10eab..63962d792 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -1,6 +1,5 @@ use std::cell::Cell; - -use crate::innerlude::*; +use std::fmt::Display; // create a cell with a "none" value #[inline] @@ -64,3 +63,28 @@ impl Future for YieldNow { } } } + +/// A component's unique identifier. +/// +/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `ScopeId` will be reused for a new component. +#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(pub usize); + +/// An Element's unique identifier. +/// +/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `ElementId` will be reused for a new component. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ElementId(pub usize); +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ElementId { + pub fn as_u64(self) -> u64 { + self.0 as u64 + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b43907ea2..1c732d862 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,4 +1,5 @@ //! # VirtualDOM Implementation for Rust +//! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. //! //! In this file, multiple items are defined. This file is big, but should be documented well to @@ -19,22 +20,39 @@ //! Additional functionality is defined in the respective files. use crate::innerlude::*; -use futures_util::{Future, FutureExt}; -use std::{ - any::{Any, TypeId}, - pin::Pin, - rc::Rc, -}; +use futures_util::Future; +use std::any::Any; /// An integrated virtual node system that progresses events and diffs UI trees. +/// /// Differences are converted into patches which a renderer can use to draw the UI. /// +/// If you are building an App with Dioxus, you probably won't want to reach for this directly, instead opting to defer +/// to a particular crate's wrapper over the [`VirtualDom`] API. /// +/// Example +/// ```rust +/// static App: FC<()> = |cx, props|{ +/// cx.render(rsx!{ +/// div { +/// "Hello World" +/// } +/// }) +/// } /// +/// async fn main() { +/// let mut dom = VirtualDom::new(App); +/// let mut inital_edits = dom.rebuild(); +/// initialize_screen(inital_edits); /// -/// -/// -/// +/// loop { +/// let next_frame = TimeoutFuture::new(Duration::from_millis(16)); +/// let edits = dom.run_with_deadline(next_frame).await; +/// apply_edits(edits); +/// render_frame(); +/// } +/// } +/// ``` pub struct VirtualDom { scheduler: Scheduler, @@ -42,7 +60,10 @@ pub struct VirtualDom { root_fc: Box, - root_props: Pin>, + root_props: Box, + + // we need to keep the allocation around, but we don't necessarily use it + _root_caller: Box Fn(&'b Scope) -> DomTree<'b> + 'static>, } impl VirtualDom { @@ -101,15 +122,23 @@ impl VirtualDom { /// let mutations = dom.rebuild(); /// ``` pub fn new_with_props(root: FC

, root_props: P) -> Self { - let scheduler = Scheduler::new(); + let root_fc = Box::new(root); + + let root_props: Box = Box::new(root_props); - let root_props: Pin> = Box::pin(root_props); let props_ptr = root_props.downcast_ref::

().unwrap() as *const P; + // Safety: this callback is only valid for the lifetime of the root props + let root_caller: Box DomTree> = Box::new(move |scope: &Scope| unsafe { + let props: &'_ P = &*(props_ptr as *const P); + std::mem::transmute(root(Context { scope }, props)) + }); + + let scheduler = Scheduler::new(); + let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { - let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); Scope::new( - caller, + root_caller.as_ref(), myidx, None, 0, @@ -119,7 +148,8 @@ impl VirtualDom { }); Self { - root_fc: Box::new(root), + _root_caller: root_caller, + root_fc, base_scope, scheduler, root_props, @@ -151,26 +181,38 @@ impl VirtualDom { /// struct AppProps { /// route: &'static str /// } - /// static App: FC = |cx| cx.render(rsx!{ "route is {cx.route}" }); + /// static App: FC = |cx, props|cx.render(rsx!{ "route is {cx.route}" }); /// /// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" }); /// /// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap(); /// ``` - pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option> { + pub fn update_root_props

(&mut self, root_props: P) -> Option + where + P: 'static, + { let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); + + // Pre-emptively drop any downstream references of the old props root_scope.ensure_drop_safety(&self.scheduler.pool); - let mut root_props: Pin> = Box::pin(root_props); + let mut root_props: Box = Box::new(root_props); if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { + // Swap the old props and new props std::mem::swap(&mut self.root_props, &mut root_props); let root = *self.root_fc.downcast_ref::>().unwrap(); - let new_caller = NodeFactory::create_component_caller(root, props_ptr as *const _); + let root_caller: Box DomTree> = + Box::new(move |scope: &Scope| unsafe { + let props: &'_ P = &*(props_ptr as *const P); + std::mem::transmute(root(Context { scope }, props)) + }); - root_scope.update_scope_dependencies(new_caller, ScopeChildren(&[])); + root_scope.update_scope_dependencies(&root_caller, ScopeChildren(&[])); + + drop(root_props); Some(self.rebuild()) } else { @@ -178,81 +220,70 @@ impl VirtualDom { } } - /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch /// - /// The diff machine expects the RealDom's stack to be the root of the application + /// The diff machine expects the RealDom's stack to be the root of the application. /// - /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed - /// through "run". We completely avoid the task scheduler infrastructure. - pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> { - let mut fut = self.rebuild_async().boxed_local(); - - loop { - if let Some(edits) = (&mut fut).now_or_never() { - break edits; - } - } - } - - /// Rebuild the dom from the ground up + /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the + /// root component will be ran once and then diffed. All updates will flow out as mutations. /// - /// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing - /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This - /// method can be useful when needing to perform some crucial periodic tasks. - pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { - let mut shared = self.scheduler.pool.clone(); - let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); - - let cur_component = self - .scheduler - .pool - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); - - // We run the component. If it succeeds, then we can diff it and add the changes to the dom. - if cur_component.run_scope(&self.scheduler.pool) { - diff_machine - .stack - .create_node(cur_component.frames.fin_head(), MountType::Append); - diff_machine.stack.scope_stack.push(self.base_scope); - - // let completed = diff_machine.work(); - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); - } - - unsafe { std::mem::transmute(diff_machine.mutations) } + /// All state stored in components will be completely wiped away. + /// + /// # Example + /// ``` + /// static App: FC<()> = |cx, props|cx.render(rsx!{ "hello world" }); + /// let mut dom = VirtualDom::new(); + /// let edits = dom.rebuild(); + /// + /// apply_edits(edits); + /// ``` + pub fn rebuild(&mut self) -> Mutations { + self.scheduler.rebuild(self.base_scope) } /// Compute a manual diff of the VirtualDOM between states. /// /// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event. - pub fn diff<'s>(&'s mut self) -> Mutations<'s> { - let cur_component = self - .scheduler - .pool - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); - - if cur_component.run_scope(&self.scheduler.pool) { - let mut diff_machine: DiffMachine<'s> = - DiffMachine::new(Mutations::new(), &mut self.scheduler.pool); - diff_machine.diff_scope(self.base_scope); - diff_machine.mutations - } else { - Mutations::new() - } + /// + /// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used + /// to force an update of the DOM when the state of the app is changed outside of the app. + /// + /// + /// # Example + /// ```rust + /// #[derive(PartialEq, Props)] + /// struct AppProps { + /// value: Shared<&'static str>, + /// } + /// + /// static App: FC = |cx, props|{ + /// let val = cx.value.borrow(); + /// cx.render(rsx! { div { "{val}" } }) + /// }; + /// + /// let value = Rc::new(RefCell::new("Hello")); + /// let mut dom = VirtualDom::new_with_props( + /// App, + /// AppProps { + /// value: value.clone(), + /// }, + /// ); + /// + /// let _ = dom.rebuild(); + /// + /// *value.borrow_mut() = "goodbye"; + /// + /// let edits = dom.diff(); + /// ``` + pub fn diff(&mut self) -> Mutations { + self.scheduler.hard_diff(self.base_scope) } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. /// /// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will /// return "None" - pub fn run_immediate<'s>(&'s mut self) -> Option>> { + pub fn run_immediate(&mut self) -> Option> { if self.scheduler.has_any_work() { Some(self.scheduler.work_sync()) } else { @@ -287,7 +318,7 @@ impl VirtualDom { /// # Example /// /// ```no_run - /// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} ); + /// static App: FC<()> = |cx, props|rsx!(cx, div {"hello"} ); /// let mut dom = VirtualDom::new(App); /// loop { /// let deadline = TimeoutFuture::from_ms(16); @@ -303,29 +334,76 @@ impl VirtualDom { /// applied the edits. /// /// Mutations are the only link between the RealDOM and the VirtualDOM. - pub async fn run_with_deadline<'s>( - &'s mut self, - deadline: impl Future, - ) -> Vec> { - let mut deadline = Box::pin(deadline.fuse()); - self.scheduler.work_with_deadline(deadline).await + pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec> { + self.scheduler.work_with_deadline(deadline) } pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { self.scheduler.pool.channel.sender.clone() } - pub fn has_work(&self) -> bool { - true - } + /// Waits for the scheduler to have work + /// This lets us poll async tasks during idle periods without blocking the main thread. + pub async fn wait_for_work(&mut self) { + if self.scheduler.has_any_work() { + log::debug!("No need to wait for work, we already have some"); + return; + } - pub async fn wait_for_any_work(&mut self) { - let mut timeout = Box::pin(futures_util::future::pending().fuse()); - self.scheduler.wait_for_any_trigger(&mut timeout).await; + log::debug!("No active work.... waiting for some..."); + use futures_util::StreamExt; + + // right now this won't poll events if there is ongoing work + // in the future we want to prioritize some events over ongoing work + // this is coming in the "priorities" PR + + // Wait for any new events if we have nothing to do + // todo: poll the events once even if there is work to do to prevent starvation + futures_util::select! { + _ = self.scheduler.async_tasks.next() => {} + msg = self.scheduler.receiver.next() => { + match msg.unwrap() { + SchedulerMsg::Task(t) => todo!(), + SchedulerMsg::Immediate(im) => { + self.scheduler.dirty_scopes.insert(im); + } + SchedulerMsg::UiEvent(evt) => { + self.scheduler.ui_events.push_back(evt); + } + } + }, + } + + while let Ok(Some(msg)) = self.scheduler.receiver.try_next() { + match msg { + SchedulerMsg::Task(t) => todo!(), + SchedulerMsg::Immediate(im) => { + self.scheduler.dirty_scopes.insert(im); + } + SchedulerMsg::UiEvent(evt) => { + self.scheduler.ui_events.push_back(evt); + } + } + } } } -// TODO! -// These impls are actually wrong. The DOM needs to have a mutex implemented. -unsafe impl Sync for VirtualDom {} -unsafe impl Send for VirtualDom {} +impl std::fmt::Display for VirtualDom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let base = self.base_scope(); + let root = base.root_node(); + + let renderer = ScopeRenderer { + show_fragments: false, + skip_components: false, + + _scope: base, + _pre_render: false, + _newline: true, + _indent: true, + _max_depth: usize::MAX, + }; + + renderer.render(self, root, f, 0) + } +} diff --git a/packages/core/tests/README.md b/packages/core/tests/README.md new file mode 100644 index 000000000..63293c741 --- /dev/null +++ b/packages/core/tests/README.md @@ -0,0 +1,53 @@ +# Testing of Dioxus core + + +NodeFactory +- [] rsx, html, NodeFactory generate the same structures + +Diffing +- [x] create elements +- [x] create text +- [x] create fragments +- [x] create empty fragments (placeholders) +- [x] diff elements +- [x] diff from element/text to fragment +- [x] diff from element/text to empty fragment +- [x] diff to element with children works too +- [x] replace with works forward +- [x] replace with works backward +- [x] un-keyed diffing +- [x] keyed diffing +- [x] keyed diffing out of order +- [x] keyed diffing with prefix/suffix +- [x] suspended nodes work + +Lifecycle +- [] Components mount properly +- [] Components create new child components +- [] Replaced components unmount old components and mount new +- [] Post-render effects are called +- [] + + +Shared Context +- [] Shared context propagates downwards +- [] unwrapping shared context if it doesn't exist works too + +Suspense +- [] use_suspense generates suspended nodes + + +Hooks +- [] Drop order is maintained +- [] Shared hook state is okay +- [] use_hook works +- [] use_ref works +- [] use_noderef works +- [] use_provide_state +- [] use_consume_state + + +VirtualDOM API +- [] work +- [] rebuild +- [] change props diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs new file mode 100644 index 000000000..02fdd51c5 --- /dev/null +++ b/packages/core/tests/borrowedstate.rs @@ -0,0 +1,44 @@ +use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; + +static Parent: FC<()> = |cx, props| { + let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {}); + + cx.render(rsx! { + div { + Child { name: value } + Child { name: value } + Child { name: value } + Child { name: value } + } + }) +}; + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a String, +} + +fn Child<'a>(cx: Context<'a>, props: &'a ChildProps) -> DomTree<'a> { + cx.render(rsx! { + div { + h1 { "it's nested" } + Child2 { name: props.name } + } + }) +} + +#[derive(Props)] +struct Grandchild<'a> { + name: &'a String, +} + +fn Child2<'a>(cx: Context<'a>, props: &Grandchild) -> DomTree<'a> { + cx.render(rsx! { + div { "Hello {props.name}!" } + }) +} + +#[test] +fn test_borrowed_state() {} diff --git a/packages/core/tests/channels.rs b/packages/core/tests/channels.rs deleted file mode 100644 index 2eb64e5b1..000000000 --- a/packages/core/tests/channels.rs +++ /dev/null @@ -1,19 +0,0 @@ -use futures_channel::mpsc::unbounded; - -#[async_std::test] -async fn channels() { - let (sender, mut receiver) = unbounded::(); - - // drop(sender); - - match receiver.try_next() { - Ok(a) => { - dbg!(a); - } - Err(no) => { - dbg!(no); - } - } - - sender.unbounded_send(1).unwrap(); -} diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_dom.rs similarity index 75% rename from packages/core/tests/create_iterative.rs rename to packages/core/tests/create_dom.rs index d95b3b0dd..b1b6639b8 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_dom.rs @@ -1,17 +1,24 @@ -//! tests to prove that the iterative implementation works +//! Prove that the dom works normally through virtualdom methods. +//! +//! This methods all use "rebuild" which completely bypasses the scheduler. +//! Hard rebuilds don't consume any events from the event queue. -use anyhow::{Context, Result}; -use dioxus::{prelude::*, DomEdit, Mutations}; -mod test_logging; +use dioxus::{prelude::*, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; + +mod test_logging; use DomEdit::*; -const LOGGING_ENABLED: bool = false; +fn new_dom(app: FC

, props: P) -> VirtualDom { + const IS_LOGGING_ENABLED: bool = false; + test_logging::set_up_logging(IS_LOGGING_ENABLED); + VirtualDom::new_with_props(app, props) +} #[test] fn test_original_diff() { - static App: FC<()> = |cx| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { @@ -21,7 +28,7 @@ fn test_original_diff() { }) }; - let mut dom = VirtualDom::new(App); + let mut dom = new_dom(APP, ()); let mutations = dom.rebuild(); assert_eq!( mutations.edits, @@ -39,9 +46,9 @@ fn test_original_diff() { ); } -#[async_std::test] -async fn create() { - static App: FC<()> = |cx| { +#[test] +fn create() { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { @@ -59,9 +66,9 @@ async fn create() { }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(APP, ()); + let mutations = dom.rebuild(); + assert_eq!( mutations.edits, [ @@ -90,9 +97,9 @@ async fn create() { ); } -#[async_std::test] -async fn create_list() { - static App: FC<()> = |cx| { +#[test] +fn create_list() { + static APP: FC<()> = |cx, props| { cx.render(rsx! { {(0..3).map(|f| rsx!{ div { "hello" @@ -100,10 +107,8 @@ async fn create_list() { }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(APP, ()); + let mutations = dom.rebuild(); // copilot wrote this test :P assert_eq!( @@ -132,9 +137,9 @@ async fn create_list() { ); } -#[async_std::test] -async fn create_simple() { - static App: FC<()> = |cx| { +#[test] +fn create_simple() { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div {} div {} @@ -143,10 +148,8 @@ async fn create_simple() { }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(APP, ()); + let mutations = dom.rebuild(); // copilot wrote this test :P assert_eq!( @@ -160,10 +163,9 @@ async fn create_simple() { ] ); } - -#[async_std::test] -async fn create_components() { - static App: FC<()> = |cx| { +#[test] +fn create_components() { + static App: FC<()> = |cx, props| { cx.render(rsx! { Child { "abc1" } Child { "abc2" } @@ -171,7 +173,7 @@ async fn create_components() { }) }; - static Child: FC<()> = |cx| { + static Child: FC<()> = |cx, props| { cx.render(rsx! { h1 {} div { {cx.children()} } @@ -179,10 +181,8 @@ async fn create_components() { }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(App, ()); + let mutations = dom.rebuild(); assert_eq!( mutations.edits, @@ -215,20 +215,17 @@ async fn create_components() { ] ); } - -#[async_std::test] -async fn anchors() { - static App: FC<()> = |cx| { +#[test] +fn anchors() { + static App: FC<()> = |cx, props| { cx.render(rsx! { {true.then(|| rsx!{ div { "hello" } })} {false.then(|| rsx!{ div { "goodbye" } })} }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(App, ()); + let mutations = dom.rebuild(); assert_eq!( mutations.edits, [ @@ -244,17 +241,21 @@ async fn anchors() { ); } -#[async_std::test] -async fn suspended() { - static App: FC<()> = |cx| { - let val = use_suspense(cx, || async {}, |cx, _| cx.render(rsx! { "hi "})); +#[test] +fn suspended() { + static App: FC<()> = |cx, props| { + let val = use_suspense( + cx, + || async { + // + }, + |cx, _| cx.render(rsx! { "hi "}), + ); cx.render(rsx! { {val} }) }; - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - let mutations = dom.rebuild_async().await; + let mut dom = new_dom(App, ()); + let mutations = dom.rebuild(); assert_eq!( mutations.edits, diff --git a/packages/core/tests/debugdiff.rs b/packages/core/tests/debugdiff.rs deleted file mode 100644 index 9f6aac6c1..000000000 --- a/packages/core/tests/debugdiff.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// A virtualdom wrapper used for testing purposes. -pub struct DebugDiff {} diff --git a/packages/core/tests/diff_iterative.rs b/packages/core/tests/diff_iterative.rs deleted file mode 100644 index 0e1a9b1b5..000000000 --- a/packages/core/tests/diff_iterative.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! tests to prove that the iterative implementation works - -use dioxus::prelude::*; - -mod test_logging; -use dioxus_core as dioxus; -use dioxus_html as dioxus_elements; - -const LOGGING_ENABLED: bool = false; - -#[async_std::test] -async fn test_iterative_create_components() { - static App: FC<()> = |cx| { - // test root fragments - cx.render(rsx! { - Child { "abc1" } - Child { "abc2" } - Child { "abc3" } - }) - }; - - fn Child(cx: Context<()>) -> DomTree { - // test root fragments, anchors, and ChildNode type - cx.render(rsx! { - h1 {} - div { {cx.children()} } - Fragment { - Fragment { - Fragment { - "wozza" - } - } - } - {(0..0).map(|_f| rsx!{ div { "walalla"}})} - p {} - }) - } - - test_logging::set_up_logging(LOGGING_ENABLED); - - let mut dom = VirtualDom::new(App); - - let mutations = dom.rebuild_async().await; - dbg!(mutations); - - let mutations = dom.diff(); - dbg!(mutations); -} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 2f58eb0ae..fe952c82f 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -4,24 +4,19 @@ //! //! It does not validated that component lifecycles work properly. This is done in another test file. -use dioxus::{prelude::*, DomEdit, TestDom}; +use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; mod test_logging; -use DomEdit::*; - -// logging is wired up to the test harness -// feel free to enable while debugging -const IS_LOGGING_ENABLED: bool = false; fn new_dom() -> TestDom { + const IS_LOGGING_ENABLED: bool = false; test_logging::set_up_logging(IS_LOGGING_ENABLED); TestDom::new() } -#[test] -fn diffing_works() {} +use DomEdit::*; /// Should push the text node onto the stack and modify it #[test] @@ -98,8 +93,8 @@ fn fragments_create_properly() { fn empty_fragments_create_anchors() { let dom = new_dom(); - let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); - let right = rsx!({ (0..1).map(|f| rsx! { div {}}) }); + let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); + let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) }); let (create, change) = dom.lazy_diff(left, right); @@ -121,8 +116,8 @@ fn empty_fragments_create_anchors() { fn empty_fragments_create_many_anchors() { let dom = new_dom(); - let left = rsx!({ (0..0).map(|f| rsx! { div {}}) }); - let right = rsx!({ (0..5).map(|f| rsx! { div {}}) }); + let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); + let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) }); let (create, change) = dom.lazy_diff(left, right); assert_eq!( @@ -148,7 +143,7 @@ fn empty_fragments_create_many_anchors() { fn empty_fragments_create_anchors_with_many_children() { let dom = new_dom(); - let left = rsx!({ (0..0).map(|f| rsx! { div {} }) }); + let left = rsx!({ (0..0).map(|_| rsx! { div {} }) }); let right = rsx!({ (0..3).map(|f| { rsx! { div { "hello: {f}" }} @@ -192,11 +187,11 @@ fn many_items_become_fragment() { let dom = new_dom(); let left = rsx!({ - (0..2).map(|f| { + (0..2).map(|_| { rsx! { div { "hello" }} }) }); - let right = rsx!({ (0..0).map(|f| rsx! { div {} }) }); + let right = rsx!({ (0..0).map(|_| rsx! { div {} }) }); let (create, change) = dom.lazy_diff(left, right); assert_eq!( @@ -235,17 +230,17 @@ fn two_equal_fragments_are_equal() { let dom = new_dom(); let left = rsx!({ - (0..2).map(|f| { + (0..2).map(|_| { rsx! { div { "hello" }} }) }); let right = rsx!({ - (0..2).map(|f| { + (0..2).map(|_| { rsx! { div { "hello" }} }) }); - let (create, change) = dom.lazy_diff(left, right); + let (_create, change) = dom.lazy_diff(left, right); assert!(change.edits.is_empty()); } @@ -263,7 +258,7 @@ fn two_fragments_with_differrent_elements_are_differet() { p {} ); - let (create, changes) = dom.lazy_diff(left, right); + let (_create, changes) = dom.lazy_diff(left, right); log::debug!("{:#?}", &changes); assert_eq!( changes.edits, @@ -700,3 +695,21 @@ fn controlled_keyed_diffing_out_of_order_max_test() { ] ); } + +#[test] +fn suspense() { + let dom = new_dom(); + + let edits = dom.create(LazyNodes::new(|f| { + use std::cell::{Cell, RefCell}; + VNode::Suspended(f.bump().alloc(VSuspended { + task_id: 0, + callback: RefCell::new(None), + dom_id: Cell::new(None), + })) + })); + assert_eq!( + edits.edits, + [CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }] + ); +} diff --git a/packages/core/tests/display_vdom.rs b/packages/core/tests/display_vdom.rs new file mode 100644 index 000000000..461a9068b --- /dev/null +++ b/packages/core/tests/display_vdom.rs @@ -0,0 +1,42 @@ +//! test that we can display the virtualdom properly +//! +//! +//! + +use std::{cell::RefCell, rc::Rc}; + +use anyhow::{Context, Result}; +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; +mod test_logging; + +const IS_LOGGING_ENABLED: bool = true; + +#[test] +fn please_work() { + static App: FC<()> = |cx, props| { + cx.render(rsx! { + div { + hidden: "true" + "hello" + div { "hello" } + Child {} + Child {} + Child {} + } + div { "hello" } + }) + }; + + static Child: FC<()> = |cx, props| { + cx.render(rsx! { + div { "child" } + }) + }; + + let mut dom = VirtualDom::new(App); + dom.rebuild(); + + println!("{}", dom); +} diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 9c7748e44..ccdd03810 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -7,7 +7,7 @@ use dioxus_html as dioxus_elements; #[async_std::test] async fn event_queue_works() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { cx.render(rsx! { div { "hello world" } }) diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 66b2f5906..a12f9a570 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,11 +1,16 @@ +use std::{cell::RefCell, rc::Rc}; + use anyhow::{Context, Result}; use dioxus::prelude::*; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; +type Shared = Rc>; + #[test] fn sample_refs() { - // static App: FC<()> = |cx| { + + // static App: FC<()> = |cx, props|{ // let div_ref = use_node_ref::(cx); // cx.render(rsx! { diff --git a/packages/core/tests/integration.rs b/packages/core/tests/integration.rs deleted file mode 100644 index 1e43dd699..000000000 --- a/packages/core/tests/integration.rs +++ /dev/null @@ -1,6 +0,0 @@ - -// type VirtualNode = VNode; - -/// Test a basic usage of a virtual dom + text renderer combo -#[test] -fn simple_integration() {} diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs new file mode 100644 index 000000000..6faf8d9e4 --- /dev/null +++ b/packages/core/tests/lifecycle.rs @@ -0,0 +1,43 @@ +//! Tests for the lifecycle of components. + +use std::{cell::RefCell, rc::Rc}; + +use anyhow::{Context, Result}; +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; +mod test_logging; + +const IS_LOGGING_ENABLED: bool = true; +type Shared = Rc>; + +#[test] +fn manual_diffing() { + test_logging::set_up_logging(IS_LOGGING_ENABLED); + + #[derive(PartialEq, Props)] + struct AppProps { + value: Shared<&'static str>, + } + + static App: FC = |cx, props| { + let val = props.value.borrow(); + cx.render(rsx! { div { "{val}" } }) + }; + + let value = Rc::new(RefCell::new("Hello")); + let mut dom = VirtualDom::new_with_props( + App, + AppProps { + value: value.clone(), + }, + ); + + let _ = dom.rebuild(); + + *value.borrow_mut() = "goodbye"; + + let edits = dom.diff(); + + log::debug!("edits: {:?}", edits); +} diff --git a/packages/core/tests/scheduler.rs b/packages/core/tests/scheduler.rs new file mode 100644 index 000000000..f058dfe4c --- /dev/null +++ b/packages/core/tests/scheduler.rs @@ -0,0 +1,6 @@ +//! Tests for the scheduler. +//! +//! TODO +//! - priority lanes +//! - periodic checking +//! diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs new file mode 100644 index 000000000..b34980b64 --- /dev/null +++ b/packages/core/tests/sharedstate.rs @@ -0,0 +1,41 @@ +use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom}; +use dioxus_core as dioxus; +use dioxus_html as dioxus_elements; +use DomEdit::*; + +mod test_logging; + +fn new_dom() -> TestDom { + const IS_LOGGING_ENABLED: bool = false; + test_logging::set_up_logging(IS_LOGGING_ENABLED); + TestDom::new() +} + +#[test] +fn shared_state_test() { + struct MySharedState(&'static str); + + static App: FC<()> = |cx, props| { + cx.use_provide_state(|| MySharedState("world!")); + rsx!(cx, Child {}) + }; + + static Child: FC<()> = |cx, props| { + let shared = cx.use_consume_state::()?; + rsx!(cx, "Hello, {shared.0}") + }; + + let mut dom = VirtualDom::new(App); + let Mutations { edits, .. } = dom.rebuild(); + + assert_eq!( + edits, + [ + CreateTextNode { + id: 0, + text: "Hello, world!" + }, + AppendChildren { many: 1 }, + ] + ); +} diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 3cd985f11..13f30a0eb 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -14,7 +14,7 @@ use dioxus_html as dioxus_elements; #[test] fn app_runs() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { // cx.render(rsx!( div{"hello"} )) }; @@ -25,7 +25,7 @@ fn app_runs() { #[test] fn fragments_work() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { cx.render(rsx!( div{"hello"} div{"goodbye"} @@ -39,7 +39,7 @@ fn fragments_work() { #[test] fn lists_work() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {(0..6).map(|f| rsx!(span{ "{f}" }))} @@ -52,7 +52,7 @@ fn lists_work() { #[test] fn conditional_rendering() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {true.then(|| rsx!(span{ "a" }))} @@ -69,13 +69,13 @@ fn conditional_rendering() { #[test] fn child_components() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { cx.render(rsx!( {true.then(|| rsx!(Child { }))} {false.then(|| rsx!(Child { }))} )) }; - static Child: FC<()> = |cx| { + static Child: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} h1 {"goodbye"} @@ -88,7 +88,7 @@ fn child_components() { #[test] fn suspended_works() { - static App: FC<()> = |cx| { + static App: FC<()> = |cx, props| { let title = use_suspense(cx, || async { "bob" }, |cx, f| cx.render(rsx! { "{f}"})); cx.render(rsx!("hello" { title })) }; diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs deleted file mode 100644 index b36c65b1a..000000000 --- a/packages/core/tests/work_sync.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Diffing is interruptible, but uses yield_now which is loop-pollable -//! -//! This means you can actually call it synchronously if you want. - -use anyhow::{Context, Result}; -use dioxus::{prelude::*, scope::Scope}; -use dioxus_core as dioxus; -use dioxus_html as dioxus_elements; -use futures_util::FutureExt; - -#[test] -fn worksync() { - static App: FC<()> = |cx| { - cx.render(rsx! { - div {"hello"} - }) - }; - let mut dom = VirtualDom::new(App); - - let mut fut = dom.rebuild_async().boxed_local(); - - let mutations = loop { - let g = (&mut fut).now_or_never(); - if g.is_some() { - break g.unwrap(); - } - }; - - dbg!(mutations); -} diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 56817e0b6..dc840b09d 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -8,7 +8,7 @@ Dioxus-webview is an attempt at making a simpler "Tauri" where creating desktop // main.rs #[async_std::main] async fn main() { - dioxus_desktop::new(|cx| { + dioxus_desktop::new(|cx, props|{ let (count, set_count) = use_state(cx, || 0); cx.render(html! {

diff --git a/packages/desktop/examples/test.rs b/packages/desktop/examples/test.rs index cebd522b5..b327b1c33 100644 --- a/packages/desktop/examples/test.rs +++ b/packages/desktop/examples/test.rs @@ -6,7 +6,7 @@ fn main() { dioxus_desktop::launch(App, |f| f.with_window(|w| w.with_maximized(true))).expect("Failed"); } -static App: FC<()> = |cx| { +static App: FC<()> = |cx, props|{ // cx.render(rsx!( div { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index c9c7153b4..436103a70 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -64,10 +64,12 @@ impl WebviewRenderer { Self::run_with_edits(root, props, user_builder, None) } - pub fn run_with_edits( + pub fn run_with_edits< + F: for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>, + >( root: FC, props: T, - user_builder: impl for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>, + user_builder: F, redits: Option>>, ) -> anyhow::Result<()> { log::info!("hello edits"); diff --git a/packages/hooks/examples/lifecycle.rs b/packages/hooks/examples/lifecycle.rs index 87c4e5559..0398f7b16 100644 --- a/packages/hooks/examples/lifecycle.rs +++ b/packages/hooks/examples/lifecycle.rs @@ -10,7 +10,7 @@ fn main() {} // Remvoe, // } -// static Component: FC<()> = |cx| { +// static Component: FC<()> = |cx, props|{ // let (tasks, dispatch) = use_reducer( // cx, // || CompState { tasks: Vec::new() }, diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 97c759b33..6a0722d95 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,2 +1,5 @@ mod usestate; -pub use usestate::use_state; +pub use usestate::{use_state, AsyncUseState, UseState}; + +mod useref; +pub use useref::*; diff --git a/packages/hooks/src/usecollection.rs b/packages/hooks/src/usecollection.rs index cb696a214..d0b3b71e7 100644 --- a/packages/hooks/src/usecollection.rs +++ b/packages/hooks/src/usecollection.rs @@ -36,7 +36,7 @@ uses the same memoization on top of the use_context API. Here's a fully-functional todo app using the use_map API: ```rust -static TodoList: FC<()> = |cx| { +static TodoList: FC<()> = |cx, props|{ let todos = use_map(cx, || HashMap::new()); let input = use_ref(|| None); diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index e69de29bb..1639628e0 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -0,0 +1,38 @@ +use std::cell::{Ref, RefCell, RefMut}; + +use dioxus_core::Context; + +pub struct UseRef<'a, T> { + inner: &'a RefCell, +} + +impl<'a, T> UseRef<'a, T> { + pub fn read(&self) -> Ref<'_, T> { + self.inner.borrow() + } + + pub fn read_write(&self) -> (Ref<'_, T>, &Self) { + (self.read(), self) + } + + /// Calling "write" will force the component to re-render + pub fn write(&self) -> RefMut<'_, T> { + self.inner.borrow_mut() + } + + /// Allows the ability to write the value without forcing a re-render + pub fn write_silent(&self) -> RefMut<'_, T> { + self.inner.borrow_mut() + } +} + +impl Clone for UseRef<'_, T> { + fn clone(&self) -> Self { + Self { inner: self.inner } + } +} +impl Copy for UseRef<'_, T> {} + +pub fn use_ref(cx: Context, f: impl FnOnce() -> T) -> UseRef { + cx.use_hook(|_| RefCell::new(f()), |f| UseRef { inner: f }, |_| {}) +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index f0d3abc43..10015a23c 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -35,7 +35,7 @@ use std::{ /// /// Usage: /// ```ignore -/// const Example: FC<()> = |cx| { +/// const Example: FC<()> = |cx, props|{ /// let counter = use_state(cx, || 0); /// let increment = |_| counter += 1; /// let decrement = |_| counter += 1; @@ -49,14 +49,14 @@ use std::{ /// } /// } /// ``` -pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>( - cx: Context<'a, P>, - initial_state_fn: F, +pub fn use_state<'a, 'c, T: 'static>( + cx: Context<'a>, + initial_state_fn: impl FnOnce() -> T, ) -> UseState<'a, T> { cx.use_hook( move |_| UseStateInner { current_val: initial_state_fn(), - callback: cx.schedule_update(), + update_callback: cx.schedule_update(), wip: Rc::new(RefCell::new(None)), update_scheuled: Cell::new(false), }, @@ -75,13 +75,14 @@ pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>( struct UseStateInner { current_val: T, update_scheuled: Cell, - callback: Rc, + update_callback: Rc, wip: Rc>>, } pub struct UseState<'a, T: 'static> { inner: &'a UseStateInner, } + impl Copy for UseState<'_, T> {} impl<'a, T> Clone for UseState<'a, T> where @@ -97,13 +98,13 @@ impl<'a, T: 'static> UseState<'a, T> { pub fn needs_update(&self) { if !self.inner.update_scheuled.get() { self.inner.update_scheuled.set(true); - (self.inner.callback)(); + (self.inner.update_callback)(); } } pub fn set(&self, new_val: T) { - self.needs_update(); *self.inner.wip.borrow_mut() = Some(new_val); + self.needs_update(); } pub fn get(&self) -> &'a T { @@ -115,6 +116,11 @@ impl<'a, T: 'static> UseState<'a, T> { self.inner.wip.borrow() } + /// Get the current status of the work-in-progress data + pub fn get_wip_mut(&self) -> RefMut> { + self.inner.wip.borrow_mut() + } + pub fn classic(self) -> (&'a T, &'a Rc) { todo!() } @@ -136,7 +142,12 @@ impl<'a, T: 'static> UseState<'a, T> { } impl<'a, T: 'static + ToOwned> UseState<'a, T> { - pub fn get_mut(self) -> RefMut<'a, T> { + /// Gain mutable access to the new value. 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(); diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 819a5d291..1d21ae0b7 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path="../core" } +dioxus-core = { path = "../core" } [dev-dependencies] scraper = "0.12.0" diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index b726d34a5..714969007 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -43,6 +43,7 @@ macro_rules! style_trait_methods { )* ) => { $( + #[inline] $(#[$attr])* fn $name<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr($lit, val, Some("style"), false) @@ -112,6 +113,13 @@ pub trait GlobalAttributes { title; translate; } + + // This macro creates an explicit method call for each of the style attributes. + // + // The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the + // actual name of the attribute generated. + // + // This roughly follows the html spec style_trait_methods! { /// Specifies the alignment of flexible container's items within the flex container. align_content: "align-content", @@ -152,34 +160,33 @@ pub trait GlobalAttributes { /// Specifies whether or not the "back" side of a transformed element is visible when facing the user. backface_visibility: "backface-visibility", - /// Defines a variety of background properties within one declaration. background: "background", /// Specify whether the background image is fixed in the viewport or scrolls. background_attachment: "background-attachment", + /// Specifies the painting area of the background. background_clip: "background-clip", - /// Defines an element's background color. background_color: "background-color", /// Defines an element's background image. background_image: "background-image", + /// Specifies the positioning area of the background images. background_origin: "background-origin", - /// Defines the origin of a background image. background_position: "background-position", /// Specify whether/how the background image is tiled. background_repeat: "background-repeat", + /// Specifies the size of the background images. background_size: "background-size", - /// Sets the width, style, and color for all four sides of an element's border. border: "border", @@ -188,13 +195,13 @@ pub trait GlobalAttributes { /// Sets the color of the bottom border of an element. border_bottom_color: "border-bottom-color", + /// Defines the shape of the bottom_left border corner of an element. border_bottom_left_radius: "border-bottom-left-radius", /// Defines the shape of the bottom_right border corner of an element. border_bottom_right_radius: "border-bottom-right-radius", - /// Sets the style of the bottom border of an element. border_bottom_style: "border-bottom-style", @@ -206,6 +213,7 @@ pub trait GlobalAttributes { /// Sets the color of the border on all the four sides of an element. border_color: "border-color", + /// Specifies how an image is to be used in place of the border styles. border_image: "border-image", @@ -224,7 +232,6 @@ pub trait GlobalAttributes { /// Specifies the width of the image_border. border_image_width: "border-image-width", - /// Sets the width, style, and color of the left border of an element. border_left: "border-left", @@ -236,10 +243,10 @@ pub trait GlobalAttributes { /// Sets the width of the left border of an element. border_left_width: "border-left-width", + /// Defines the shape of the border corners of an element. border_radius: "border-radius", - /// Sets the width, style, and color of the right border of an element. border_right: "border-right", @@ -263,13 +270,13 @@ pub trait GlobalAttributes { /// Sets the color of the top border of an element. border_top_color: "border-top-color", + /// Defines the shape of the top_left border corner of an element. border_top_left_radius: "border-top-left-radius", /// Defines the shape of the top_right border corner of an element. border_top_right_radius: "border-top-right-radius", - /// Sets the style of the top border of an element. border_top_style: "border-top-style", @@ -281,13 +288,13 @@ pub trait GlobalAttributes { /// Specify the location of the bottom edge of the positioned element. bottom: "bottom", + /// Applies one or more drop_shadows to the element's box. box_shadow: "box-shadow", /// Alter the default CSS box model. box_sizing: "box-sizing", - /// Specify the position of table's caption. caption_side: "caption-side", @@ -299,6 +306,7 @@ pub trait GlobalAttributes { /// Specify the color of the text of an element. color: "color", + /// Specifies the number of columns in a multi_column element. column_count: "column-count", @@ -329,7 +337,6 @@ pub trait GlobalAttributes { /// A shorthand property for setting column_width and column_count properties. columns: "columns", - /// Inserts generated content. content: "content", @@ -350,6 +357,7 @@ pub trait GlobalAttributes { /// Show or hide borders and backgrounds of empty table cells. empty_cells: "empty-cells", + /// Specifies the components of a flexible length. flex: "flex", @@ -371,7 +379,6 @@ pub trait GlobalAttributes { /// Specifies whether the flexible items should wrap or not. flex_wrap: "flex-wrap", - /// Specifies whether or not a box should float. float: "float", @@ -383,13 +390,13 @@ pub trait GlobalAttributes { /// Defines the font size for the text. font_size: "font-size", + /// Preserves the readability of text when font fallback occurs. font_size_adjust: "font-size-adjust", /// Selects a normal, condensed, or expanded face from a font. font_stretch: "font-stretch", - /// Defines the font style for the text. font_style: "font-style", @@ -401,10 +408,10 @@ pub trait GlobalAttributes { /// Specify the height of an element. height: "height", + /// Specifies how flex items are aligned along the main axis of the flex container after any flexible lengths and auto margins have been resolved. justify_content: "auto margins have been resolved.", - /// Specify the location of the left edge of the positioned element. left: "left", @@ -452,22 +459,22 @@ pub trait GlobalAttributes { /// Specify the minimum width of an element. min_width: "min-width", + /// Specifies the transparency of an element. opacity: "opacity", /// Specifies the order in which a flex items are displayed and laid out within a flex container. order: "order", - /// Sets the width, style, and color for all four sides of an element's outline. outline: "outline", /// Sets the color of the outline. outline_color: "outline-color", + /// Set the space between an outline and the border edge of an element. outline_offset: "outline-offset", - /// Sets a style for an outline. outline_style: "outline-style", @@ -482,7 +489,6 @@ pub trait GlobalAttributes { /// Specifies the treatment of content that overflows the element's box vertically. overflow_y: "overflow-y", - /// Sets the padding on all four sides of the element. padding: "padding", @@ -506,24 +512,25 @@ pub trait GlobalAttributes { /// Insert a page breaks inside an element. page_break_inside: "page-break-inside", + /// Defines the perspective from which all child elements of the object are viewed. perspective: "perspective", /// Defines the origin (the vanishing point for the 3D space) for the perspective property. perspective_origin: "perspective-origin", - /// Specifies how an element is positioned. position: "position", /// Specifies quotation marks for embedded quotations. quotes: "quotes", + /// Specifies whether or not an element is resizable by the user. resize: "resize", - /// Specify the location of the right edge of the positioned element. right: "right", + /// Specifies the length of the tab character. tab_size: "tab-size", @@ -546,9 +553,9 @@ pub trait GlobalAttributes { /// Specifies the style of the lines specified by the text_decoration_line property text_decoration_style: "text-decoration-style", - /// Indent the first line of text. text_indent: "text-indent", + /// Specifies the justification method to use when the text_align property is set to justify. text_justify: "text-justify", @@ -558,12 +565,12 @@ pub trait GlobalAttributes { /// Applies one or more shadows to the text content of an element. text_shadow: "text-shadow", - /// Transforms the case of the text. text_transform: "text-transform", /// Specify the location of the top edge of the positioned element. top: "top", + /// Applies a 2D or 3D transformation to an element. transform: "transform", @@ -588,7 +595,6 @@ pub trait GlobalAttributes { /// Specifies the speed curve of the transition effect. transition_timing_function: "transition-timing-function", - /// Sets the vertical positioning of an element relative to the current text baseline. vertical_align: "vertical-align", @@ -600,12 +606,13 @@ pub trait GlobalAttributes { /// Specify the width of an element. width: "width", + /// Specifies how to break lines within words. word_break: "word-break", - /// Sets the spacing between words. word_spacing: "word-spacing", + /// Specifies whether to break words when the content overflows the boundaries of its container. word_wrap: "word-wrap", @@ -674,7 +681,10 @@ macro_rules! builder_constructors { $( $(#[$attr:meta])* $name:ident { - $($fil:ident: $vil:ident,)* + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* }; )* ) => { @@ -692,6 +702,7 @@ macro_rules! builder_constructors { impl $name { $( + $(#[$attr_method])* pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr(stringify!($fil), val, None, false) } @@ -767,6 +778,7 @@ builder_constructors! { sizes: String, // FIXME title: String, // FIXME r#type: Mime, + integrity: String, }; /// Build a @@ -1518,7 +1530,10 @@ builder_constructors! { src: Uri, step: String, tabindex: usize, - r#type: InputType, + + // This has a manual implementation below + // r#type: InputType, + value: String, width: isize, }; @@ -1563,7 +1578,10 @@ builder_constructors! { option { disabled: Bool, label: String, - selected: Bool, + + // defined below + // selected: Bool, + value: String, }; @@ -1588,7 +1606,8 @@ builder_constructors! { /// [`