diff --git a/README.md b/README.md index f27f8236f..9b0060b43 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust. ```rust -fn App((cx, props): Scope<()>) -> Element { +fn App(cx: Context, props: &()) -> Element { let mut count = use_state(cx, || 0); cx.render(rsx!( @@ -141,16 +141,16 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | NodeRef | ✅ | ✅ | gain direct access to nodes | | Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | | Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | -| Cooperative Scheduling | ✅ | ✅ | Prioritize important events over non-important events | +| Effects | ✅ | ✅ | Run effects after a component has been committed to render | +| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | +| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server | +| Bundle Splitting | 👀 | ✅ | Efficiently and asynchronously load the app | +| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded | +| 1st class global state | ✅ | ✅ | redux/recoil/mobx on top of context | | Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | | Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | | Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | | Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations | -| Effects | 🛠 | ✅ | Run effects after a component has been committed to render | -| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server | -| Bundle Splitting | 👀 | ✅ | Hybrid components for SPA and Server | -| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded | | Fine-grained reactivity | 👀 | ❓ | Skip diffing for fine-grain updates | - ✅ = implemented and working diff --git a/examples/async.rs b/examples/async.rs index c9abce7c2..b0ea0a7b4 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -17,7 +17,7 @@ pub static App: FC<()> = |(cx, _)| { let (async_count, dir) = (count.for_async(), *direction); - let (task, _) = use_task(cx, move || async move { + let (task, _) = use_coroutine(cx, move || async move { loop { TimeoutFuture::new(250).await; *async_count.get_mut() += dir; diff --git a/examples/borrowed.rs b/examples/borrowed.rs index dc4805500..786de9da9 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -21,7 +21,7 @@ fn main() { } fn App((cx, props): Scope<()>) -> Element { - let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); + let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); let first = text.get_mut(0).unwrap(); diff --git a/examples/core/async.rs b/examples/core/async.rs index 24a4f34f7..a21f82652 100644 --- a/examples/core/async.rs +++ b/examples/core/async.rs @@ -5,7 +5,7 @@ fn main() { dom.rebuild(); } -const App: FC<()> = |(cx, props)| { +const App: FC<()> = |cx, props| { let id = cx.scope_id(); // cx.submit_task(Box::pin(async move { id })); diff --git a/examples/core/contextapi.rs b/examples/core/contextapi.rs index c9f839f4a..6c90a60c7 100644 --- a/examples/core/contextapi.rs +++ b/examples/core/contextapi.rs @@ -6,7 +6,7 @@ struct SomeContext { } #[allow(unused)] -static Example: FC<()> = |(cx, props)| { +static Example: FC<()> = |cx, props| { todo!() // let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap()); diff --git a/examples/core/syntax.rs b/examples/core/syntax.rs index bda8c8f4d..5eec9d181 100644 --- a/examples/core/syntax.rs +++ b/examples/core/syntax.rs @@ -33,7 +33,7 @@ fn html_usage() { static App2: FC<()> = |(cx, _)| cx.render(rsx!("hello world!")); -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let name = cx.use_state(|| 0); cx.render(rsx!(div { @@ -99,7 +99,7 @@ impl<'a> Children<'a> { } } -static Bapp: FC<()> = |(cx, props)| { +static Bapp: FC<()> = |cx, props| { let name = cx.use_state(|| 0); cx.render(rsx!( @@ -114,7 +114,7 @@ static Bapp: FC<()> = |(cx, props)| { )) }; -static Match: FC<()> = |(cx, props)| { +static Match: FC<()> = |cx, props| { // let b: Box VNode> = Box::new(|f| todo!()); diff --git a/examples/core/vdom_usage.rs b/examples/core/vdom_usage.rs index 8f5d278b1..cbe3c324f 100644 --- a/examples/core/vdom_usage.rs +++ b/examples/core/vdom_usage.rs @@ -5,7 +5,7 @@ use dioxus_core::{lazynodes::LazyNodes, prelude::*}; // #[async_std::main] fn main() { static App: FC<()> = - |(cx, props)| cx.render(Some(LazyNodes::new(move |f| f.text(format_args!("hello"))))); + |cx, props| cx.render(Some(LazyNodes::new(move |f| f.text(format_args!("hello"))))); let mut dom = VirtualDom::new(App); diff --git a/examples/core_reference/antipatterns.rs b/examples/core_reference/antipatterns.rs index 522f99a76..735a57104 100644 --- a/examples/core_reference/antipatterns.rs +++ b/examples/core_reference/antipatterns.rs @@ -32,7 +32,7 @@ use dioxus::prelude::*; struct NoKeysProps { data: std::collections::HashMap, } -static AntipatternNoKeys: FC = |(cx, props)| { +static AntipatternNoKeys: FC = |cx, props| { // WRONG: Make sure to add keys! rsx!(cx, ul { {props.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} @@ -54,7 +54,7 @@ static AntipatternNoKeys: FC = |(cx, props)| { /// /// 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, props)| { +static AntipatternNestedFragments: FC<()> = |cx, props| { // Try to avoid heavily nesting fragments rsx!(cx, Fragment { @@ -82,7 +82,7 @@ static AntipatternNestedFragments: FC<()> = |(cx, props)| { /// 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 AntipatternRelyingOnSetState: FC<()> = |(cx, props)| { +static AntipatternRelyingOnSetState: 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 AntipatternRelyingOnSetState: FC<()> = |(cx, props)| { /// - All components must start with an uppercase character /// /// i.e.: the following component will be rejected when attempted to be used in the rsx! macro -static antipattern_component: FC<()> = |(cx, props)| todo!(); +static antipattern_component: FC<()> = |cx, props| todo!(); /// Antipattern: Misusing hooks /// --------------------------- @@ -120,7 +120,7 @@ static antipattern_component: FC<()> = |(cx, props)| todo!(); struct MisuedHooksProps { should_render_state: bool, } -static AntipatternMisusedHooks: FC = |(cx, props)| { +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 @@ -153,7 +153,7 @@ static AntipatternMisusedHooks: FC = |(cx, props)| { /// } /// } /// }) -static _example: FC<()> = |(cx, props)| todo!(); +static _example: FC<()> = |cx, props| todo!(); /// Antipattern: publishing components and hooks with all features enabled /// ---------------------------------------------------------------------- @@ -171,9 +171,9 @@ static _example: FC<()> = |(cx, props)| 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, props)| todo!(); +static __example: FC<()> = |cx, props| todo!(); -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { AntipatternNoKeys { data: std::collections::HashMap::new() } AntipatternNestedFragments {} diff --git a/examples/core_reference/basics.rs b/examples/core_reference/basics.rs index 81a252cb7..6a3807bb2 100644 --- a/examples/core_reference/basics.rs +++ b/examples/core_reference/basics.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { Greeting { @@ -25,7 +25,7 @@ struct GreetingProps { name: &'static str, } -static Greeting: FC = |(cx, props)| { +static Greeting: FC = |cx, props| { cx.render(rsx! { div { h1 { "Hello, {props.name}!" } diff --git a/examples/core_reference/children.rs b/examples/core_reference/children.rs index 0340e2dcb..bdd3108d0 100644 --- a/examples/core_reference/children.rs +++ b/examples/core_reference/children.rs @@ -18,7 +18,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { Banner { @@ -31,7 +31,7 @@ pub static Example: FC<()> = |(cx, props)| { }) }; -pub static Banner: FC<()> = |(cx, props)| { +pub static Banner: FC<()> = |cx, props| { cx.render(rsx! { div { h1 { "This is a great banner!" } diff --git a/examples/core_reference/conditional_rendering.rs b/examples/core_reference/conditional_rendering.rs index b30869265..6a5fe9a64 100644 --- a/examples/core_reference/conditional_rendering.rs +++ b/examples/core_reference/conditional_rendering.rs @@ -16,7 +16,7 @@ use dioxus::prelude::*; pub struct MyProps { should_show: bool, } -pub static Example0: FC = |(cx, props)| { +pub static Example0: FC = |cx, props| { cx.render(rsx! { div { {props.should_show.then(|| rsx!{ @@ -39,7 +39,7 @@ pub static Example0: FC = |(cx, props)| { pub struct MyProps1 { should_show: bool, } -pub static Example1: FC = |(cx, props)| { +pub static Example1: FC = |cx, props| { cx.render(rsx! { div { // With matching @@ -77,7 +77,7 @@ pub enum Color { pub struct MyProps2 { color: Color, } -pub static Example2: FC = |(cx, props)| { +pub static Example2: FC = |cx, props| { cx.render(rsx! { div { {match props.color { @@ -89,7 +89,7 @@ pub static Example2: FC = |(cx, props)| { }) }; -pub static Example: FC<()> = |(cx, props)| { +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/core_reference/controlled_inputs.rs b/examples/core_reference/controlled_inputs.rs index b4c056b6f..bdabe83d2 100644 --- a/examples/core_reference/controlled_inputs.rs +++ b/examples/core_reference/controlled_inputs.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { @@ -10,7 +10,7 @@ pub static Example: FC<()> = |(cx, props)| { }; // A controlled component: -static ControlledSelect: FC<()> = |(cx, props)| { +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, props)| { }; // TODO - how do uncontrolled things work? -static UncontrolledSelect: FC<()> = |(cx, props)| { +static UncontrolledSelect: FC<()> = |cx, props| { let value = use_state(cx, || String::new()); cx.render(rsx! { diff --git a/examples/core_reference/custom_elements.rs b/examples/core_reference/custom_elements.rs index 090dabd50..053b3aea5 100644 --- a/examples/core_reference/custom_elements.rs +++ b/examples/core_reference/custom_elements.rs @@ -11,7 +11,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { custom_element { diff --git a/examples/core_reference/empty.rs b/examples/core_reference/empty.rs index 52bc80640..28914bd6b 100644 --- a/examples/core_reference/empty.rs +++ b/examples/core_reference/empty.rs @@ -5,4 +5,4 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| cx.render(rsx! { Fragment {} }); +pub static Example: FC<()> = |cx, props| cx.render(rsx! { Fragment {} }); diff --git a/examples/core_reference/errorhandling.rs b/examples/core_reference/errorhandling.rs index dd34a5f3f..e598726e5 100644 --- a/examples/core_reference/errorhandling.rs +++ b/examples/core_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, props)| { +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, props)| { +static App1: FC<()> = |cx, props| { let data = match get_data() { Some(data) => data, None => return None, @@ -46,7 +46,7 @@ static App1: FC<()> = |(cx, props)| { /// a user is logged in. /// /// Dioxus will throw an error in the console if the None-path is ever taken. -static App2: FC<()> = |(cx, props)| { +static App2: FC<()> = |cx, props| { let data = get_data()?; cx.render(rsx!( div { "{data}" } )) }; @@ -54,14 +54,14 @@ static App2: FC<()> = |(cx, props)| { /// This is top-tier error handling since it displays a failure state. /// /// However, the error is lacking in context. -static App3: FC<()> = |(cx, props)| 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 to short-circuit the match-based error handling with `.ok()` which converts /// a Result into an Option and lets you abort rendering by early-returning `None` -static App4: FC<()> = |(cx, props)| { +static App4: FC<()> = |cx, props| { let data = get_data_err().ok()?; cx.render(rsx!( div { "{data}" } )) }; @@ -69,7 +69,7 @@ static App4: FC<()> = |(cx, props)| { /// This is great error handling since it displays a failure state... with context! /// /// Hopefully you'll never need to display a screen like this. It's rather bad taste -static App5: FC<()> = |(cx, props)| 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/core_reference/fragments.rs b/examples/core_reference/fragments.rs index af888d021..88c9dcaea 100644 --- a/examples/core_reference/fragments.rs +++ b/examples/core_reference/fragments.rs @@ -11,7 +11,7 @@ use dioxus::prelude::*; // Returning multiple elements with rsx! or html! -static App1: FC<()> = |(cx, props)| { +static App1: FC<()> = |cx, props| { cx.render(rsx! { h1 { } h2 { } @@ -20,7 +20,7 @@ static App1: FC<()> = |(cx, props)| { }; // Using the Fragment component -static App2: FC<()> = |(cx, props)| { +static App2: FC<()> = |cx, props| { cx.render(rsx! { Fragment { div {} @@ -31,7 +31,7 @@ static App2: FC<()> = |(cx, props)| { }; // Using the `fragment` method on the NodeFactory -static App3: FC<()> = |(cx, props)| { +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, props)| { })) }; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { App1 {} App2 {} diff --git a/examples/core_reference/global_css.rs b/examples/core_reference/global_css.rs index 67f3c2bbb..5e7c75a8c 100644 --- a/examples/core_reference/global_css.rs +++ b/examples/core_reference/global_css.rs @@ -19,7 +19,7 @@ h1 {color: blue;} p {color: red;} "#; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { head { style { "{STYLE}" } } body { diff --git a/examples/core_reference/inline_styles.rs b/examples/core_reference/inline_styles.rs index 2161812ff..e3a45154c 100644 --- a/examples/core_reference/inline_styles.rs +++ b/examples/core_reference/inline_styles.rs @@ -10,7 +10,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { head { style: { background_color: "powderblue" } @@ -29,7 +29,7 @@ pub static Example: FC<()> = |(cx, props)| { // .... technically the rsx! macro is slightly broken at the moment and allows 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, props)| { +static Example2: FC<()> = |cx, props| { cx.render(rsx! { div { color: "red" "hello world!" diff --git a/examples/core_reference/iterators.rs b/examples/core_reference/iterators.rs index cd216401e..f8feae8a8 100644 --- a/examples/core_reference/iterators.rs +++ b/examples/core_reference/iterators.rs @@ -12,7 +12,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { let example_data = use_state(cx, || 0); let v = (0..10).map(|f| { diff --git a/examples/core_reference/listener.rs b/examples/core_reference/listener.rs index 3cd13edfb..63813e423 100644 --- a/examples/core_reference/listener.rs +++ b/examples/core_reference/listener.rs @@ -7,7 +7,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { ButtonList {} NonUpdatingEvents {} @@ -16,7 +16,7 @@ pub static Example: FC<()> = |(cx, props)| { }; /// We can use `set_name` in multiple closures; the closures automatically *copy* the reference to set_name. -static ButtonList: FC<()> = |(cx, props)| { +static ButtonList: FC<()> = |cx, props| { let name = use_state(cx, || "...?"); let names = ["jack", "jill", "john", "jane"] @@ -33,7 +33,7 @@ static ButtonList: FC<()> = |(cx, props)| { /// This shows how listeners may be without a visible change in the display. /// Check the console. -static NonUpdatingEvents: FC<()> = |(cx, props)| { +static NonUpdatingEvents: FC<()> = |cx, props| { rsx!(cx, div { button { onclick: move |_| log::info!("Did not cause any updates!") @@ -42,7 +42,7 @@ static NonUpdatingEvents: FC<()> = |(cx, props)| { }) }; -static DisablePropagation: FC<()> = |(cx, props)| { +static DisablePropagation: FC<()> = |cx, props| { rsx!(cx, div { onclick: move |_| log::info!("event propagated to the div!") diff --git a/examples/core_reference/memo.rs b/examples/core_reference/memo.rs index 1532b5ab8..40aa54191 100644 --- a/examples/core_reference/memo.rs +++ b/examples/core_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, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { "100% memoized!" } }) @@ -35,7 +35,7 @@ pub struct MyProps1 { name: String, } -pub static Example1: FC = |(cx, props)| { +pub static Example1: FC = |cx, props| { cx.render(rsx! { div { "100% memoized! {props.name}" } }) @@ -49,7 +49,7 @@ pub struct MyProps2 { name: std::rc::Rc, } -pub static Example2: FC = |(cx, props)| { +pub static Example2: FC = |cx, props| { cx.render(rsx! { div { "100% memoized! {props.name}" } }) diff --git a/examples/core_reference/noderefs.rs b/examples/core_reference/noderefs.rs index 021fcfa59..8d22ee5b4 100644 --- a/examples/core_reference/noderefs.rs +++ b/examples/core_reference/noderefs.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { let p = 10; cx.render(rsx! { diff --git a/examples/core_reference/signals.rs b/examples/core_reference/signals.rs index 742285005..2e69d8e66 100644 --- a/examples/core_reference/signals.rs +++ b/examples/core_reference/signals.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/core_reference/spreadpattern.rs b/examples/core_reference/spreadpattern.rs index 56bde5aed..58a813cb5 100644 --- a/examples/core_reference/spreadpattern.rs +++ b/examples/core_reference/spreadpattern.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { let props = MyProps { count: 0, live: true, diff --git a/examples/core_reference/statemanagement.rs b/examples/core_reference/statemanagement.rs index 742285005..2e69d8e66 100644 --- a/examples/core_reference/statemanagement.rs +++ b/examples/core_reference/statemanagement.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; fn main() {} -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/core_reference/suspense.rs b/examples/core_reference/suspense.rs index 2a89e97ee..e3bbd2574 100644 --- a/examples/core_reference/suspense.rs +++ b/examples/core_reference/suspense.rs @@ -14,7 +14,7 @@ struct DogApi { } const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random"; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { let doggo = use_suspense( cx, || surf::get(ENDPOINT).recv_json::(), diff --git a/examples/core_reference/task.rs b/examples/core_reference/task.rs index cb70881d2..a5e877bc6 100644 --- a/examples/core_reference/task.rs +++ b/examples/core_reference/task.rs @@ -24,7 +24,7 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { let count = use_state(cx, || 0); let mut direction = use_state(cx, || 1); diff --git a/examples/core_reference/testing.rs b/examples/core_reference/testing.rs index 4ce33c5f1..587ce19cc 100644 --- a/examples/core_reference/testing.rs +++ b/examples/core_reference/testing.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -pub static Example: FC<()> = |(cx, props)| { +pub static Example: FC<()> = |cx, props| { cx.render(rsx! { div { diff --git a/examples/core_reference/tostring.rs b/examples/core_reference/tostring.rs index dd10c658b..a04a165da 100644 --- a/examples/core_reference/tostring.rs +++ b/examples/core_reference/tostring.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; use dioxus::ssr; -pub static Example: FC<()> = |(cx, props)| { +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, props)| { }) }; -static SomeApp: FC<()> = |(cx, props)| { +static SomeApp: FC<()> = |cx, props| { cx.render(rsx! { div { style: {background_color: "blue"} h1 {"Some amazing app or component"} diff --git a/examples/coroutine.rs b/examples/coroutine.rs index 3cafeb055..c2e025e16 100644 --- a/examples/coroutine.rs +++ b/examples/coroutine.rs @@ -27,18 +27,18 @@ fn main() { use dioxus::prelude::*; -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let p1 = use_state(cx, || 0); let p2 = use_state(cx, || 0); let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async()); - let (p1_handle, _) = use_task(cx, || async move { + let (p1_handle, _) = use_coroutine(cx, || async move { loop { *p1_async.get_mut() += 1; async_std::task::sleep(std::time::Duration::from_millis(75)).await; } }); - let (p2_handle, _) = use_task(cx, || async move { + let (p2_handle, _) = use_coroutine(cx, || async move { loop { *p2_async.get_mut() += 1; async_std::task::sleep(std::time::Duration::from_millis(100)).await; diff --git a/examples/crm.rs b/examples/crm.rs index be1abb3d9..e0c873221 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -90,15 +90,17 @@ static App: FC<()> = |(cx, _)| { } }; - 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} - }) + cx.render(rsx!( + 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/desktop/demo.rs b/examples/desktop/demo.rs index 2b3367b22..77a927e99 100644 --- a/examples/desktop/demo.rs +++ b/examples/desktop/demo.rs @@ -8,7 +8,7 @@ fn main() { dioxus_desktop::launch(App, |c| c); } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { cx.render(rsx!( div { "hello world!" diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 096922a21..113f52216 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -18,7 +18,7 @@ fn main() { }); } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let file_manager = use_ref(cx, || Files::new()); let files = file_manager.read(); diff --git a/examples/hydration.rs b/examples/hydration.rs index 494b6654b..f50f740ac 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -19,7 +19,7 @@ fn main() { dioxus::desktop::launch(App, |c| c.with_prerendered(content)); } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let mut val = use_state(cx, || 0); cx.render(rsx! { diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 0accd1702..d20477e81 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -31,7 +31,7 @@ fn main() { }); } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let state = use_ref(cx, || Calculator::new()); let clear_display = state.read().display_value.eq("0"); diff --git a/examples/readme.rs b/examples/readme.rs index b1e29a985..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, props)| { +static App: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); cx.render(rsx! { diff --git a/examples/router.rs b/examples/router.rs index e04f92ebd..5b021a807 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -23,7 +23,7 @@ pub enum Route { NotFound, } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let route = use_router(cx, Route::parse)?; cx.render(rsx! { diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index becf386f5..c0d69535e 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, props)| { +pub static Example: FC<()> = |cx, props| { let formatting = "formatting!"; let formatting_tuple = ("a", "b"); let lazy_fmt = format_args!("lazily formatted text"); @@ -102,12 +102,9 @@ pub static Example: FC<()> = |(cx, props)| { }} // Matching - // Matching will throw a Rust error about "no two closures are the same type" - // 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* unhygienic macros) {match true { - true => rsx!(cx, h1 {"Top text"}), - false => cx.render(rsx!( h1 {"Bottom text"})) + true => rsx!( h1 {"Top text"}), + false => rsx!( h1 {"Bottom text"}) }} // Conditional rendering @@ -174,10 +171,17 @@ pub static Example: FC<()> = |(cx, props)| { // Can take children too! Taller { a: "asd", div {"hello world!"} } + + // helper functions + {helper(cx, "hello world!")} } }) }; +fn helper(cx: Context, text: &str) -> Element { + rsx!(cx, p { "{text}" }) +} + mod baller { use super::*; #[derive(Props, PartialEq)] diff --git a/examples/ssr.rs b/examples/ssr.rs index 2823e00fc..539db8653 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, props)| { +static App: FC<()> = |cx, props| { cx.render(rsx!( div { h1 { "Title" } @@ -21,6 +21,6 @@ static App: FC<()> = |(cx, props)| { struct MyProps<'a> { text: &'a str, } -fn App2<'a>(cx: Context<'a>, props: &'a MyProps) -> Element<'a> { +fn App2(cx: Context, props: &MyProps) -> Element { None } diff --git a/examples/ssr/basic.rs b/examples/ssr/basic.rs index a4be3a701..c89c425e3 100644 --- a/examples/ssr/basic.rs +++ b/examples/ssr/basic.rs @@ -12,7 +12,7 @@ fn main() { ) } -pub static App: FC<()> = |(cx, props)| { +pub static App: FC<()> = |cx, props| { cx.render(rsx!( div { class: "overflow-hidden" diff --git a/examples/ssr/tide.rs b/examples/ssr/tide.rs index eada5dae6..d86fbdf55 100644 --- a/examples/ssr/tide.rs +++ b/examples/ssr/tide.rs @@ -42,7 +42,7 @@ fn main() {} // initial_name: String, // } -// static Example: FC = |(cx, props)| { +// static Example: FC = |cx, props| { // let dispaly_name = use_state(cx, move || props.initial_name.clone()); // cx.render(rsx! { diff --git a/examples/ssr/tofile.rs b/examples/ssr/tofile.rs index 35777240f..e3091580a 100644 --- a/examples/ssr/tofile.rs +++ b/examples/ssr/tofile.rs @@ -24,7 +24,7 @@ fn main() { .unwrap(); } -pub static App: FC<()> = |(cx, props)| { +pub static App: FC<()> = |cx, props| { cx.render(rsx!( div { class: "overflow-hidden" link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } @@ -39,7 +39,7 @@ pub static App: FC<()> = |(cx, props)| { )) }; -pub static Header: FC<()> = |(cx, props)| { +pub static Header: FC<()> = |cx, props| { cx.render(rsx! { div { header { class: "text-gray-400 bg-gray-900 body-font" @@ -65,7 +65,7 @@ pub static Header: FC<()> = |(cx, props)| { }) }; -pub static Hero: FC<()> = |(cx, props)| { +pub static Hero: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -103,7 +103,7 @@ pub static Hero: FC<()> = |(cx, props)| { } }) }; -pub static Entry: FC<()> = |(cx, props)| { +pub static Entry: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -116,7 +116,7 @@ pub static Entry: FC<()> = |(cx, props)| { }) }; -pub static StacksIcon: FC<()> = |(cx, props)| { +pub static StacksIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { xmlns: "http://www.w3.org/2000/svg" @@ -131,7 +131,7 @@ pub static StacksIcon: FC<()> = |(cx, props)| { } )) }; -pub static RightArrowIcon: FC<()> = |(cx, props)| { +pub static RightArrowIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { fill: "none" diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 47046be8f..fe7100762 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, props)| { +pub static App: FC<()> = |cx, props| { cx.render(rsx!( div { class: "overflow-hidden" style { "{STYLE}" } @@ -30,7 +30,7 @@ pub static App: FC<()> = |(cx, props)| { )) }; -pub static Header: FC<()> = |(cx, props)| { +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, props)| { }) }; -pub static Hero: FC<()> = |(cx, props)| { +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, props)| { } }) }; -pub static Entry: FC<()> = |(cx, props)| { +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, props)| { }) }; -pub static StacksIcon: FC<()> = |(cx, props)| { +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, props)| { } )) }; -pub static RightArrowIcon: FC<()> = |(cx, props)| { +pub static RightArrowIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { fill: "none" diff --git a/examples/tasks.rs b/examples/tasks.rs new file mode 100644 index 000000000..a7daca839 --- /dev/null +++ b/examples/tasks.rs @@ -0,0 +1,23 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; +fn main() { + dioxus::desktop::launch(App, |c| c); +} + +static App: FC<()> = |cx, props| { + let mut count = use_state(cx, || 0); + + cx.push_task(async { + panic!("polled future"); + // + }); + + cx.render(rsx! { + div { + h1 { "High-Five counter: {count}" } + } + }) +}; diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 6ed2c1d7d..cfc49f530 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -22,7 +22,7 @@ pub struct TodoItem { } const STYLE: &str = include_str!("./assets/todomvc.css"); -const App: FC<()> = |(cx, props)| { +const App: FC<()> = |cx, props| { let mut draft = use_state(cx, || "".to_string()); let mut todos = use_state(cx, || HashMap::>::new()); let mut filter = use_state(cx, || FilterState::All); @@ -85,7 +85,7 @@ pub struct TodoEntryProps { todo: Rc, } -pub fn TodoEntry((cx, props): Scope) -> Element { +pub fn TodoEntry(cx: Context, props: &TodoEntryProps) -> Element { let mut is_editing = use_state(cx, || false); let mut contents = use_state(cx, || String::from("")); let todo = &props.todo; diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 2682b9f9b..fca6c8cf7 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -12,7 +12,7 @@ fn main() { const ENDPOINT: &str = "https://api.openweathermap.org/data/2.5/weather"; -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { // let body = use_suspense( cx, @@ -24,21 +24,23 @@ static App: FC<()> = |(cx, props)| { .await .unwrap(); }, - |cx, props| { + |props| { // - rsx!(WeatherDisplay {}) + cx.render(rsx!(WeatherDisplay {})) }, ); - rsx!(cx, div { - {body} + cx.render(rsx! { + div { + {body} + } }) }; #[derive(PartialEq, Props)] struct WeatherProps {} -static WeatherDisplay: FC = |(cx, props)| { +static WeatherDisplay: FC = |cx, props| { // cx.render(rsx!( div { class: "flex items-center justify-center flex-col" diff --git a/examples/web/blah.rs b/examples/web/blah.rs index de4bf3cd3..d21cd5fd0 100644 --- a/examples/web/blah.rs +++ b/examples/web/blah.rs @@ -24,7 +24,7 @@ fn main() { dioxus_web::launch(App, |c| c) } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { let mut state = use_state(cx, || 0); cx.render(rsx! { div { diff --git a/examples/web/btns.rs b/examples/web/btns.rs index 59db81f3c..179074ee7 100644 --- a/examples/web/btns.rs +++ b/examples/web/btns.rs @@ -23,7 +23,7 @@ fn main() { // dioxus::web::launch(App, |c| c); } -static App: FC<()> = |(cx, props)| { +static App: FC<()> = |cx, props| { dbg!("rednering parent"); cx.render(rsx! { div { @@ -40,7 +40,7 @@ static App: FC<()> = |(cx, props)| { }) }; -static But: FC<()> = |(cx, props)| { +static But: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); // let d = Dropper { name: "asd" }; diff --git a/examples/web/demo.rs b/examples/web/demo.rs index f331560e1..603b84de5 100644 --- a/examples/web/demo.rs +++ b/examples/web/demo.rs @@ -24,7 +24,7 @@ fn main() { dioxus_web::launch(App, |c| c) } -pub static App: FC<()> = |(cx, props)| { +pub static App: FC<()> = |cx, props| { cx.render(rsx!( div { class: "overflow-hidden" link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } @@ -39,7 +39,7 @@ pub static App: FC<()> = |(cx, props)| { )) }; -pub static Header: FC<()> = |(cx, props)| { +pub static Header: FC<()> = |cx, props| { cx.render(rsx! { div { header { class: "text-gray-400 bg-gray-900 body-font" @@ -65,7 +65,7 @@ pub static Header: FC<()> = |(cx, props)| { }) }; -pub static Hero: FC<()> = |(cx, props)| { +pub static Hero: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -103,7 +103,7 @@ pub static Hero: FC<()> = |(cx, props)| { } }) }; -pub static Entry: FC<()> = |(cx, props)| { +pub static Entry: FC<()> = |cx, props| { // cx.render(rsx! { section{ class: "text-gray-400 bg-gray-900 body-font" @@ -116,7 +116,7 @@ pub static Entry: FC<()> = |(cx, props)| { }) }; -pub static StacksIcon: FC<()> = |(cx, props)| { +pub static StacksIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { // xmlns: "http://www.w3.org/2000/svg" @@ -131,7 +131,7 @@ pub static StacksIcon: FC<()> = |(cx, props)| { } )) }; -pub static RightArrowIcon: FC<()> = |(cx, props)| { +pub static RightArrowIcon: FC<()> = |cx, props| { cx.render(rsx!( svg { fill: "none" diff --git a/examples/web_tick.rs b/examples/web_tick.rs index ea2e087f8..1e60af992 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, props)| { +static App: FC<()> = |cx, props| { let mut rng = SmallRng::from_entropy(); let rows = (0..1_000).map(|f| { let label = Label::new(&mut rng); diff --git a/examples/webview_web.rs b/examples/webview_web.rs index 408095c57..debe4d8e0 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, props)| { +static App: FC<()> = |cx, props| { let mut count = use_state(cx, || 0); cx.render(rsx! { diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index c65fc9ee1..23ff3b537 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -173,7 +173,7 @@ impl ToTokens for Component { if !self.children.is_empty() { let childs = &self.children; toks.append_all(quote! { - .children(ScopeChildren::new(__cx.fragment_from_iter([ #( #childs ),* ]))) + .children(__cx.create_children([ #( #childs ),* ])) }); } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 0a13b1bcd..babaee825 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -210,12 +210,12 @@ impl ToTokens for ElementAttrNamed { ElementAttr::EventClosure { name, closure } => { quote! { - dioxus::events::on::#name(__cx, #closure) + dioxus_elements::on::#name(__cx, #closure) } } ElementAttr::EventTokens { name, tokens } => { quote! { - dioxus::events::on::#name(__cx, #tokens) + dioxus_elements::on::#name(__cx, #tokens) } } }; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 8088b55d4..94a23c6fd 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -40,8 +40,6 @@ indexmap = "1.7.0" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } -serde_repr = { version = "0.1.7", optional = true } - [dev-dependencies] anyhow = "1.0.42" dioxus-html = { path = "../html" } @@ -49,12 +47,13 @@ fern = { version = "0.6.0", features = ["colored"] } rand = { version = "0.8.4", features = ["small_rng"] } simple_logger = "1.13.0" dioxus-core-macro = { path = "../core-macro", version = "0.1.2" } +dioxus-hooks = { path = "../hooks" } # async-std = { version = "1.9.0", features = ["attributes"] } # criterion = "0.3.5" [features] default = [] -serialize = ["serde", "serde_repr"] +serialize = ["serde"] debug_vdom = [] [[bench]] diff --git a/packages/core/README.md b/packages/core/README.md index f02ec114c..45059cd89 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -40,3 +40,19 @@ We have big goals for Dioxus. The final implementation must: - Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. - Be modular. Components and hooks should be work anywhere without worrying about target platform. + + +## Safety + +Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe. + +If you don't want to use a crate that uses unsafe, then this crate is not for you. + +however, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. + +We'd also be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous variants required to execute certain features. + +There's a few invariants that are very important: + +- References to `ScopeInner` and `Props` passed into components are *always* valid for as long as the component exists. Even if the scope backing is resized to fit more scopes, the scope has to stay the same place in memory. + diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 3348f246a..a3e69b662 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -99,3 +99,105 @@ Internally, the flow of suspense works like this: 9. diff that node with the new node with a low priority on its own fiber 10. return the patches back to the event loop 11. apply the patches to the real dom + +/* +Welcome to Dioxus's cooperative, priority-based scheduler. + +I hope you enjoy your stay. + +Some essential reading: +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200 +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440 +- https://github.com/WICG/is-input-pending +- https://web.dev/rail/ +- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react + +# What's going on? + +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 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). +The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many +different types of work simultaneously. + +# How does it work? + +Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked. +React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of +React, and the UI still stays snappy! + +While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do +this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser +has time to paint and run animations. When the browser is finished, we can step in and continue our work. + +React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not +necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially +"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause +our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields +so Rust's async machinery can take over, allowing us to customize when exactly to pause it. + +React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For +now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes +before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes +are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine +future is paused and saved (self-referentially). + +# Priority System + +So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that +doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where +priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as +a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work. + +React's priority system is quite complex. + +There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only +uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on. + +For Dioxus, we're going with a 4 tier priority system: +- Sync: Things that need to be done by the next frame, like TextInput on controlled elements +- High: for events that block all others - clicks, keyboard, and hovers +- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events) +- Low: set_state called asynchronously, and anything generated by suspense + +In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed +work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave +as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to +do anything too arduous from onInput. + +For the rest, we defer to the rIC period and work down each queue from high to low. +*/ + + + +Strategy: +- When called, check for any UI events that might've been received since the last frame. +- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. + +- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high," + then we need to finish the high priority work first. If the current work is "low" then analyze what scopes + will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need + to bump the other work out of the way, or choose to process it so we don't have any conflicts. + 'static components have a leg up here since their work can be re-used among multiple scopes. + "High priority" is only for blocking! Should only be used on "clicks" + +- 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? + - should we batch? + - react says no - they are continuous + - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? + diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs index d0c8aae77..a36d4a1e5 100644 --- a/packages/core/benches/jsframework.rs +++ b/packages/core/benches/jsframework.rs @@ -24,7 +24,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_usize).map(|f| { let label = Label::new(&mut rng); @@ -58,7 +58,7 @@ struct RowProps { row_id: usize, label: Label, } -fn Row((cx, props): Scope) -> Element { +fn Row(cx: Context, props: &RowProps) -> Element { let [adj, col, noun] = props.label.0; cx.render(rsx! { tr { diff --git a/packages/core/examples/props_expand.rs b/packages/core/examples/props_expand.rs new file mode 100644 index 000000000..29d41129e --- /dev/null +++ b/packages/core/examples/props_expand.rs @@ -0,0 +1,9 @@ +use dioxus_core as dioxus; +use dioxus_core_macro::*; + +fn main() {} + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a str, +} diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs new file mode 100644 index 000000000..45104daa0 --- /dev/null +++ b/packages/core/examples/works.rs @@ -0,0 +1,44 @@ +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +fn main() { + let _ = VirtualDom::new(Parent); +} + +fn Parent(cx: Context, props: &()) -> Element { + let value = cx.use_hook(|_| String::new(), |f| &*f); + + cx.render(rsx! { + div { + Child { name: value } + Fragment { "asd" } + } + }) +} + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a str, +} + +fn Child(cx: Context, props: &ChildProps) -> Element { + cx.render(rsx! { + div { + h1 { "it's nested" } + Child2 { name: props.name } + } + }) +} + +#[derive(Props)] +struct Grandchild<'a> { + name: &'a str, +} + +fn Child2(cx: Context, props: &Grandchild) -> Element { + cx.render(rsx! { + div { "Hello {props.name}!" } + }) +} diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs deleted file mode 100644 index ac1c66189..000000000 --- a/packages/core/src/bumpframe.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::innerlude::*; -use bumpalo::Bump; -use std::cell::Cell; - -pub(crate) struct ActiveFrame { - // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken - pub generation: Cell, - - // The double-buffering situation that we will use - pub frames: [BumpFrame; 2], -} - -pub(crate) struct BumpFrame { - pub bump: Bump, - pub(crate) head_node: VNode<'static>, - - #[cfg(test)] - // used internally for debugging - _name: &'static str, -} - -impl ActiveFrame { - pub fn new() -> Self { - let b1 = Bump::new(); - let b2 = Bump::new(); - - let frame_a = BumpFrame { - head_node: VNode::Fragment(VFragment { - key: None, - children: &[], - is_static: false, - }), - bump: b1, - - #[cfg(test)] - _name: "wip", - }; - let frame_b = BumpFrame { - head_node: VNode::Fragment(VFragment { - key: None, - children: &[], - is_static: false, - }), - bump: b2, - - #[cfg(test)] - _name: "fin", - }; - Self { - generation: 0.into(), - frames: [frame_a, frame_b], - } - } - - pub unsafe fn reset_wip_frame(&mut self) { - self.wip_frame_mut().bump.reset() - } - - /// The "work in progress frame" represents the frame that is currently being worked on. - pub fn wip_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 == 0 { - true => &self.frames[0], - false => &self.frames[1], - } - } - - pub fn wip_frame_mut(&mut self) -> &mut BumpFrame { - match self.generation.get() & 1 == 0 { - true => &mut self.frames[0], - false => &mut self.frames[1], - } - } - - /// The finished frame represents the frame that has been "finished" and cannot be modified again - pub fn finished_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 == 1 { - true => &self.frames[0], - false => &self.frames[1], - } - } - - /// Give out our self-referential item with our own borrowed lifetime - pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> { - let cur_head = &self.finished_frame().head_node; - unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) } - } - - /// Give out our self-referential item with our own borrowed lifetime - pub fn wip_head<'b>(&'b self) -> &'b VNode<'b> { - let cur_head = &self.wip_frame().head_node; - unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) } - } - - pub fn cycle_frame(&mut self) { - self.generation.set(self.generation.get() + 1); - } -} - -#[cfg(test)] -mod tests { - //! These tests are bad. I don't have a good way of properly testing the ActiveFrame stuff - use super::*; - - #[test] - fn test_bump_frame() { - let mut frames = ActiveFrame::new(); - - // just cycle a few times and make sure we get the right frames out - for _ in 0..5 { - let fin = frames.finished_frame(); - let wip = frames.wip_frame(); - assert_eq!(wip._name, "wip"); - assert_eq!(fin._name, "fin"); - frames.cycle_frame(); - - let fin = frames.finished_frame(); - let wip = frames.wip_frame(); - assert_eq!(wip._name, "fin"); - assert_eq!(fin._name, "wip"); - frames.cycle_frame(); - } - assert_eq!(frames.generation.get(), 10); - } -} diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs deleted file mode 100644 index 0dc047caf..000000000 --- a/packages/core/src/childiter.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::innerlude::*; - -/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors). -/// -/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like -/// "InsertBefore". -pub(crate) struct RealChildIterator<'a> { - scopes: &'a ResourcePool, - - // Heuristically we should never bleed into 4 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - // TODO: use const generics instead of the 4 estimation - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>, -} - -impl<'a> RealChildIterator<'a> { - pub fn new(starter: &'a VNode<'a>, scopes: &'a ResourcePool) -> Self { - Self { - scopes, - stack: smallvec::smallvec![(0, starter)], - } - } -} - -impl<'a> Iterator for RealChildIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node: Option<&'a VNode<'a>> = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match &node { - // We can only exit our looping when we get "real" nodes - // This includes fragments and components when they're empty (have a single root) - VNode::Element(_) | VNode::Text(_) | VNode::Suspended(_) | VNode::Anchor(_) => { - // 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); - } - - // If we get a fragment we push the next child - VNode::Fragment(frag) => { - let subcount = *count as usize; - - if frag.children.is_empty() { - should_pop = true; - returned_node = Some(node); - } - - if subcount >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[subcount]); - } - } - - // For components, we load their root and push them onto the stack - VNode::Component(sc) => { - let scope = self - .scopes - .get_scope(sc.associated_scope.get().unwrap()) - .unwrap(); - - // Simply swap the current node on the stack with the root of the component - *node = scope.frames.fin_head(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index e249bfae1..f01e11bf7 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -5,86 +5,82 @@ //! if the type supports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder //! that ensures compile-time required and optional fields on cx. -use crate::{ - innerlude::{Context, Element, VAnchor, VFragment, VNode}, - LazyNodes, ScopeChildren, -}; -/// A component is a wrapper around a Context and some Props that share a lifetime -/// -/// -/// # Example -/// -/// With memoized state: -/// ```rust -/// struct State {} -/// -/// fn Example((cx, props): Scope) -> DomTree { -/// // ... -/// } -/// ``` -/// -/// With borrowed state: -/// ```rust -/// struct State<'a> { -/// name: &'a str -/// } -/// -/// fn Example<'a>((cx, props): Scope<'a, State>) -> DomTree<'a> { -/// // ... -/// } -/// ``` -/// -/// With owned state as a closure: -/// ```rust -/// static Example: FC<()> = |(cx, props)| { -/// // ... -/// }; -/// ``` -/// -pub type Scope<'a, T> = (Context<'a>, &'a T); +use crate::innerlude::{Context, Element, LazyNodes, NodeLink}; -pub struct FragmentProps<'a> { - children: ScopeChildren<'a>, +pub struct FragmentProps(Element); +pub struct FragmentBuilder(Element); +impl FragmentBuilder { + pub fn children(self, children: Option) -> FragmentBuilder { + FragmentBuilder(children) + } } - -pub struct FragmentBuilder<'a, const BUILT: bool> { - children: Option>, -} -impl<'a> FragmentBuilder<'a, false> { - pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> { - FragmentBuilder { - children: Some(children), - } +impl FragmentBuilder { + pub fn build(self) -> FragmentProps { + FragmentProps(self.0) } } -impl<'a, const A: bool> FragmentBuilder<'a, A> { - pub fn build(self) -> FragmentProps<'a> { - FragmentProps { - children: self.children.unwrap_or_default(), - } - } -} - -impl<'a> Properties for FragmentProps<'a> { - type Builder = FragmentBuilder<'a, false>; - +/// Access the children elements passed into the component +/// +/// This enables patterns where a component is passed children from its parent. +/// +/// ## Details +/// +/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions +/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure +/// on the props that takes Context. +/// +/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other +/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's +/// props are valid for the static lifetime. +/// +/// ## Example +/// +/// ```rust +/// fn App(cx: Context, props: &()) -> Element { +/// cx.render(rsx!{ +/// CustomCard { +/// h1 {} +/// p {} +/// } +/// }) +/// } +/// +/// #[derive(PartialEq, Props)] +/// struct CardProps { +/// children: Element +/// } +/// +/// fn CustomCard(cx: Context, props: &CardProps) -> Element { +/// cx.render(rsx!{ +/// div { +/// h1 {"Title card"} +/// {props.children} +/// } +/// }) +/// } +/// ``` +impl Properties for FragmentProps { + type Builder = FragmentBuilder; const IS_STATIC: bool = false; - fn builder() -> Self::Builder { - FragmentBuilder { children: None } + FragmentBuilder(None) } - - unsafe fn memoize(&self, other: &Self) -> bool { + unsafe fn memoize(&self, _other: &Self) -> bool { false } } /// Create inline fragments using Component syntax. /// +/// ## Details +/// /// Fragments capture a series of children without rendering extra nodes. /// -/// # Example +/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and +/// a key is needed to identify each item. +/// +/// ## Example /// /// ```rust /// rsx!{ @@ -92,20 +88,17 @@ impl<'a> Properties for FragmentProps<'a> { /// } /// ``` /// -/// # Details +/// ## Usage /// /// 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. +/// Try to avoid highly nested fragments if you can. Unlike React, 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, props): Scope<'a, FragmentProps<'a>>) -> Element<'a> { - cx.render(Some(LazyNodes::new(|f| { - f.fragment_from_iter(&props.children) - }))) +pub fn Fragment(cx: Context, props: &FragmentProps) -> Element { + cx.render(Some(LazyNodes::new(|f| f.fragment_from_iter(&props.0)))) } /// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus @@ -167,12 +160,11 @@ impl Properties for () { // that the macros use to anonymously complete prop construction. pub struct EmptyBuilder; impl EmptyBuilder { - #[inline] pub fn build(self) {} } /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder { +pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Context<'a>, &'a T) -> Element) -> T::Builder { T::builder() } diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs deleted file mode 100644 index 95953ff16..000000000 --- a/packages/core/src/context.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Public APIs for managing component state, tasks, and lifecycles. -//! -//! This module is separate from `Scope` to narrow what exactly is exposed to user code. -//! -//! We unsafely implement `send` for the VirtualDom, but those guarantees can only be - -use bumpalo::Bump; - -use crate::{innerlude::*, lazynodes::LazyNodes}; -use std::{any::TypeId, ops::Deref, rc::Rc}; - -/// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// -/// This lets components access props, schedule updates, integrate hooks, and expose shared state. -/// -/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks -/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method -/// exists for the `use_provide_state` hook to provide a shared state object. -/// -/// For the most part, the only method you should be using regularly is `render`. -/// -/// ## Example -/// -/// ```ignore -/// #[derive(Properties)] -/// struct Props { -/// name: String -/// } -/// -/// fn example(cx: Context) -> VNode { -/// html! { -///
"Hello, {cx.name}"
-/// } -/// } -/// ``` -pub struct Context<'src> { - pub scope: &'src ScopeInner, -} - -impl<'src> Copy for Context<'src> {} -impl<'src> Clone for Context<'src> { - fn clone(&self) -> Self { - 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> Deref for Context<'a> { - type Target = &'a ScopeInner; - fn deref(&self) -> &Self::Target { - &self.scope - } -} - -impl<'src> Context<'src> { - /// 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 needs_update(&self) { - (self.scope.memoized_updater)() - } - - pub fn needs_update_any(&self, id: ScopeId) { - (self.scope.shared.schedule_any_immediate)(id) - } - - /// 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 - } - - pub fn bump(&self) -> &'src Bump { - let bump = &self.scope.frames.wip_frame().bump; - bump - } - - /// 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. - /// - /// ## Example - /// - /// ```ignore - /// fn Component(cx: Context<()>) -> VNode { - /// // Lazy assemble the VNode tree - /// let lazy_tree = html! {
"Hello World"
}; - /// - /// // Actually build the tree and allocate it - /// cx.render(lazy_tree) - /// } - ///``` - pub fn render(self, lazy_nodes: Option>) -> Option> { - let bump = &self.scope.frames.wip_frame().bump; - let factory = NodeFactory { bump }; - lazy_nodes.map(|f| f.call(factory)) - } - - /// `submit_task` will submit the future to be polled. - /// - /// This is useful when you have some async task that needs to be progressed. - /// - /// This method takes ownership over the task you've provided, and must return (). This means any work that needs to - /// happen must occur within the future or scheduled for after the future completes (through schedule_update ) - /// - /// ## Explanation - /// Dioxus will step its internal event loop if the future returns if the future completes while waiting. - /// - /// Tasks can't return anything, but they can be controlled with the returned handle - /// - /// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it - /// is considered "stable" - /// - /// - /// - pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - (self.scope.shared.submit_task)(task) - } - - /// This method enables the ability to expose state to children further down the VirtualDOM Tree. - /// - /// This is a "fundamental" operation and should only be called during initialization of a hook. - /// - /// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead. - /// - /// 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_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {}); - /// rsx!(cx, Child {}) - /// } - /// - /// static Child: FC<()> = |(cx, props)|{ - /// let state = cx.consume_state::(); - /// rsx!(cx, div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_state(self, value: T) - where - T: 'static, - { - self.scope - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Rc::new(value)) - .map(|f| f.downcast::().ok()) - .flatten(); - } - - /// Try to retrieve a SharedState with type T from the any parent Scope. - pub fn consume_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().unwrap()) - } - - /// Create a new subtree with this scope as the root of the subtree. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// This method - /// - /// # Example - /// - /// ```rust - /// static App: FC<()> = |(cx, props)| { - /// todo!(); - /// rsx!(cx, div { "Subtree {id}"}) - /// }; - /// ``` - pub fn create_subtree(self) -> Option { - self.scope.new_subtree() - } - - /// Get the subtree ID that this scope belongs to. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// # Example - /// - /// ```rust - /// static App: FC<()> = |(cx, props)| { - /// let id = cx.get_current_subtree(); - /// rsx!(cx, div { "Subtree {id}"}) - /// }; - /// ``` - pub fn get_current_subtree(self) -> u32 { - self.scope.subtree() - } - - /// Store a value between renders - /// - /// This is *the* foundational hook for all other hooks. - /// - /// - Initializer: closure used to create the initial hook state - /// - 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 - /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell { - /// use_hook( - /// || Rc::new(RefCell::new(initial_value())), - /// |state| state, - /// |_| {}, - /// ) - /// } - /// ``` - pub fn use_hook( - self, - initializer: Init, - runner: Run, - cleanup: Cleanup, - ) -> Output - where - State: 'static, - Output: 'src, - Init: FnOnce(usize) -> State, - Run: FnOnce(&'src mut State) -> Output, - Cleanup: FnOnce(Box) + '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() { - self.scope.hooks.push_hook( - initializer(self.scope.hooks.len()), - Box::new(|raw| { - // - let s = raw.downcast::().unwrap(); - cleanup(s); - }), - ); - } - - runner(self.scope.hooks.next::().expect(HOOK_ERR_MSG)) - } -} - -const HOOK_ERR_MSG: &str = r###" -Unable to retrieve the hook that was initialized at 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. -Functions prefixed with "use" should never be called conditionally. -"###; diff --git a/packages/core/src/debug_dom.rs b/packages/core/src/debug_dom.rs deleted file mode 100644 index 13d874be3..000000000 --- a/packages/core/src/debug_dom.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::{innerlude::ScopeInner, virtual_dom::VirtualDom, VNode}; - -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) - } -} - -/// 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 ScopeInner, - 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); - writeln!(f, "{} {{", 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/diff.rs b/packages/core/src/diff.rs index 654048267..cdeac3839 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -90,6 +90,7 @@ use crate::innerlude::*; use fxhash::{FxHashMap, FxHashSet}; +use smallvec::{smallvec, SmallVec}; use DomEdit::*; /// Our DiffMachine is an iterative tree differ. @@ -104,76 +105,144 @@ use DomEdit::*; /// /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! -pub(crate) struct DiffMachine<'bump> { - pub vdom: &'bump ResourcePool, +pub struct DiffState<'bump> { + scopes: &'bump ScopeArena, pub mutations: Mutations<'bump>, - pub stack: DiffStack<'bump>, + pub(crate) 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 { + +impl<'bump> DiffState<'bump> { + pub(crate) fn new(scopes: &'bump ScopeArena) -> Self { Self { - force_diff: Default::default(), - } - } -} - -/// a "saved" form of a diff machine -/// in regular diff machine, the &'bump reference is a stack borrow, but the -/// bump lifetimes are heap borrows. -pub(crate) struct SavedDiffWork<'bump> { - pub mutations: Mutations<'bump>, - pub stack: DiffStack<'bump>, - pub seen_scopes: FxHashSet, -} - -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 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, - } - } -} - -impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(mutations: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { - Self { - mutations, - cfg: DiffCfg::default(), + scopes, + mutations: Mutations::new(), stack: DiffStack::new(), - vdom: shared, - seen_scopes: FxHashSet::default(), + seen_scopes: Default::default(), + force_diff: false, + } + } +} + +/// The stack instructions we use to diff and create new nodes. +#[derive(Debug)] +pub(crate) enum DiffInstruction<'a> { + Diff { + old: &'a VNode<'a>, + new: &'a VNode<'a>, + }, + + Create { + node: &'a VNode<'a>, + }, + + /// pushes the node elements onto the stack for use in mount + PrepareMove { + node: &'a VNode<'a>, + }, + + Mount { + and: MountType<'a>, + }, + + PopScope, +} + +#[derive(Debug, Clone, Copy)] +pub enum MountType<'a> { + Absorb, + Append, + Replace { old: &'a VNode<'a> }, + InsertAfter { other_node: &'a VNode<'a> }, + InsertBefore { other_node: &'a VNode<'a> }, +} + +pub(crate) struct DiffStack<'bump> { + pub(crate) instructions: Vec>, + nodes_created_stack: SmallVec<[usize; 10]>, + pub scope_stack: SmallVec<[ScopeId; 5]>, +} + +impl<'bump> DiffStack<'bump> { + pub fn new() -> Self { + Self { + instructions: Vec::with_capacity(1000), + nodes_created_stack: smallvec![], + scope_stack: smallvec![], } } - pub fn save(self) -> SavedDiffWork<'bump> { - SavedDiffWork { - mutations: self.mutations, - stack: self.stack, - seen_scopes: self.seen_scopes, + pub fn pop(&mut self) -> Option> { + self.instructions.pop() + } + + pub fn pop_off_scope(&mut self) { + self.scope_stack.pop(); + } + + pub fn push(&mut self, instruction: DiffInstruction<'bump>) { + self.instructions.push(instruction) + } + + pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { and }); + + for child in children.iter().rev() { + self.instructions + .push(DiffInstruction::Create { node: child }); } } - 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::Diff { new, old }); - self.work(|| false); - } + pub fn push_subtree(&mut self) { + self.nodes_created_stack.push(0); + self.instructions.push(DiffInstruction::Mount { + and: MountType::Append, + }); + } + + pub fn push_nodes_created(&mut self, count: usize) { + self.nodes_created_stack.push(count); + } + + 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 }); + self.instructions.push(DiffInstruction::Create { node }); + } + + pub fn add_child_count(&mut self, count: usize) { + *self.nodes_created_stack.last_mut().unwrap() += count; + } + + pub fn pop_nodes_created(&mut self) -> usize { + self.nodes_created_stack.pop().unwrap() + } + + pub fn current_scope(&self) -> Option { + self.scope_stack.last().copied() + } + + pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { + // Push the new scope onto the stack + self.scope_stack.push(idx); + + self.instructions.push(DiffInstruction::PopScope); + + // Run the creation algorithm with this scope on the stack + // ?? I think we treat components as fragments?? + self.instructions.push(DiffInstruction::Create { node }); + } +} + +impl<'bump> DiffState<'bump> { + pub fn diff_scope(&mut self, id: &ScopeId) { + let (old, new) = (self.scopes.wip_head(id), self.scopes.fin_head(id)); + self.stack.push(DiffInstruction::Diff { old, new }); + self.stack.scope_stack.push(*id); + self.stack.push_nodes_created(0); + self.work(|| false); } /// Progress the diffing for this "fiber" @@ -189,7 +258,10 @@ impl<'bump> DiffMachine<'bump> { DiffInstruction::Diff { old, new } => self.diff_node(old, new), DiffInstruction::Create { node } => self.create_node(node), DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => self.prepare_move_node(node), + DiffInstruction::PrepareMove { node } => { + let num_on_stack = self.push_all_nodes(node); + self.stack.add_child_count(num_on_stack); + } DiffInstruction::PopScope => self.stack.pop_off_scope(), }; @@ -202,10 +274,38 @@ impl<'bump> DiffMachine<'bump> { true } - fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { - for el in RealChildIterator::new(node, self.vdom) { - self.mutations.push_root(el.mounted_id()); - self.stack.add_child_count(1); + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize { + match node { + VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Linked(linked) => { + let node = unsafe { &*linked.node }; + let node: &VNode = unsafe { std::mem::transmute(node) }; + self.push_all_nodes(node) + } + + VNode::Fragment(_) | VNode::Component(_) => { + // + let mut added = 0; + for child in node.children() { + added += self.push_all_nodes(child); + } + added + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + self.mutations.push_root(el.dom_id.get().unwrap()); + + num_on_stack + 1 + } } } @@ -219,15 +319,7 @@ impl<'bump> DiffMachine<'bump> { } MountType::Replace { old } => { - if let Some(old_id) = old.try_mounted_id() { - self.mutations.replace_with(old_id, nodes_created as u32); - self.remove_nodes(Some(old), true); - } else { - if let Some(id) = self.find_first_element_id(old) { - self.mutations.replace_with(id, nodes_created as u32); - } - self.remove_nodes(Some(old), true); - } + self.replace_node(old, nodes_created); } MountType::Append => { @@ -260,18 +352,19 @@ impl<'bump> DiffMachine<'bump> { 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), + VNode::Linked(linked) => self.create_linked_node(linked), } } fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { - let real_id = self.vdom.reserve_node(node); + let real_id = self.scopes.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, node: &'bump VNode<'bump>) { - let real_id = self.vdom.reserve_node(node); + let real_id = self.scopes.reserve_node(node); self.mutations.create_placeholder(real_id); suspended.dom_id.set(Some(real_id)); @@ -281,9 +374,11 @@ impl<'bump> DiffMachine<'bump> { } fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) { - let real_id = self.vdom.reserve_node(node); + let real_id = self.scopes.reserve_node(node); + self.mutations.create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); + self.stack.add_child_count(1); } @@ -298,7 +393,7 @@ impl<'bump> DiffMachine<'bump> { .. } = element; - let real_id = self.vdom.reserve_node(node); + let real_id = self.scopes.reserve_node(node); dom_id.set(Some(real_id)); @@ -307,13 +402,13 @@ impl<'bump> DiffMachine<'bump> { self.stack.add_child_count(1); if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + let scope = self.scopes.get_scope(&cur_scope_id).unwrap(); - listeners.iter().for_each(|listener| { + for listener in *listeners { self.attach_listener_to_scope(listener, scope); listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); - }); + } } else { log::warn!("create element called with no scope on the stack - this is an error for a live dom"); } @@ -332,61 +427,47 @@ impl<'bump> DiffMachine<'bump> { } fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - 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 parent_scope = self.scopes.get_scope(&parent_idx).unwrap(); + let height = parent_scope.height + 1; + let subtree = parent_scope.subtree.get(); - let new_idx = self.vdom.insert_scope_with_key(|new_idx| { - ScopeInner::new( - caller, - new_idx, - Some(parent_idx), - parent_scope.height + 1, - parent_scope.subtree(), - shared, - ) - }); + let parent_scope = unsafe { self.scopes.get_scope_raw(&parent_idx) }; + let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) }; + let fc_ptr = vcomponent.user_fc; + + let new_idx = self + .scopes + .new_with_key(fc_ptr, caller, parent_scope, height, subtree); // Actually initialize the caller's slot with the right address vcomponent.associated_scope.set(Some(new_idx)); if !vcomponent.can_memoize { - let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap(); - let extended = vcomponent as *const VComponent; - let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) }; - cur_scope.borrowed_props.borrow_mut().push(extended); + let cur_scope = self.scopes.get_scope(&parent_idx).unwrap(); + let extended = unsafe { std::mem::transmute(vcomponent) }; + cur_scope.items.borrow_mut().borrowed_props.push(extended); } // TODO: // add noderefs to current noderef list Noderefs // add effects to current effect list Effects - - let new_component = self.vdom.get_scope_mut(new_idx).unwrap(); + let new_component = self.scopes.get_scope(&new_idx).unwrap(); log::debug!( "initializing component {:?} with height {:?}", new_idx, - parent_scope.height + 1 + height + 1 ); // Run the scope for one iteration to initialize it - if new_component.run_scope(self.vdom) { + if self.scopes.run_scope(&new_idx) { // Take the node that was just generated from running the component - let nextnode = new_component.frames.fin_head(); + let nextnode = self.scopes.fin_head(&new_idx); self.stack.create_component(new_idx, nextnode); - // - /* - tree_item { - - } - - */ if new_component.is_subtree_root.get() { self.stack.push_subtree(); } @@ -396,6 +477,16 @@ impl<'bump> DiffMachine<'bump> { self.seen_scopes.insert(new_idx); } + fn create_linked_node(&mut self, link: &'bump NodeLink) { + if link.scope_id.get().is_none() { + if let Some(cur_scope) = self.stack.current_scope() { + link.scope_id.set(Some(cur_scope)); + } + } + let node: &'bump VNode<'static> = unsafe { &*link.node }; + self.create_node(unsafe { std::mem::transmute(node) }); + } + // ================================= // Tools for diffing nodes // ================================= @@ -412,11 +503,14 @@ impl<'bump> DiffMachine<'bump> { (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new), (Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node), + (Linked(old), Linked(new)) => self.diff_linked_nodes(old, new), // Anything else is just a basic replace and create ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), + Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) + | Suspended(_), + Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) + | Suspended(_), ) => self .stack .create_node(new_node, MountType::Replace { old: old_node }), @@ -494,7 +588,7 @@ impl<'bump> DiffMachine<'bump> { // // 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(); + let scope = self.scopes.get_scope(&cur_scope_id).unwrap(); if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { @@ -533,33 +627,42 @@ impl<'bump> DiffMachine<'bump> { &mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>, - old: &'bump VComponent<'bump>, new: &'bump VComponent<'bump>, ) { let scope_addr = old.associated_scope.get().unwrap(); + log::debug!( + "Diffing components. old_scope: {:?}, old_addr: {:?}, new_addr: {:?}", + scope_addr, + old.user_fc, + new.user_fc + ); + // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { - log::debug!("Diffing component {:?} - {:?}", new.user_fc, scope_addr); - // self.stack.scope_stack.push(scope_addr); // Make sure the new component vnode is referencing the right scope id new.associated_scope.set(Some(scope_addr)); // 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); + let scope = unsafe { + self.scopes + .get_scope_mut(&scope_addr) + .expect(&format!("could not find {:?}", scope_addr)) + }; + scope.caller = unsafe { std::mem::transmute(new.caller) }; // React doesn't automatically memoize, but we do. let props_are_the_same = old.comparator.unwrap(); - 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()); + if self.force_diff || !props_are_the_same(new) { + if self.scopes.run_scope(&scope_addr) { + self.diff_node( + self.scopes.wip_head(&scope_addr), + self.scopes.fin_head(&scope_addr), + ); } } @@ -589,6 +692,19 @@ impl<'bump> DiffMachine<'bump> { self.attach_suspended_node_to_scope(new); } + fn diff_linked_nodes(&mut self, old: &'bump NodeLink, new: &'bump NodeLink) { + if !std::ptr::eq(old.node, new.node) { + // if the ptrs are the same then theyr're the same + let old: &VNode = unsafe { std::mem::transmute(&*old.node) }; + let new: &VNode = unsafe { std::mem::transmute(&*new.node) }; + self.diff_node(old, new); + } + + if new.scope_id.get().is_none() { + todo!("attach the link to the scope - when children are not created"); + } + } + // ============================================= // Utilities for creating new diff instructions // ============================================= @@ -627,7 +743,14 @@ impl<'bump> DiffMachine<'bump> { .create_children(new, MountType::Replace { old: &old[0] }); } (_, [VNode::Anchor(_)]) => { - self.replace_and_create_many_with_one(old, &new[0]); + let new: &'bump VNode<'bump> = &new[0]; + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..], true); + self.stack + .create_node(new, MountType::Replace { old: first_old }); + } else { + self.stack.create_node(new, MountType::Append {}); + } } _ => { let new_is_keyed = new[0].key().is_some(); @@ -799,6 +922,7 @@ impl<'bump> DiffMachine<'bump> { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, + old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], ) -> Option<(usize, usize)> { @@ -858,7 +982,7 @@ impl<'bump> DiffMachine<'bump> { // // This function will load the appropriate nodes onto the stack and do diffing in place. // - // Upon exit from this function, it will be restored to that same state. + // Upon exit from this function, it will be restored to that same self. fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { /* 1. Map the old keys into a numerical ordering based on indices. @@ -923,7 +1047,14 @@ impl<'bump> DiffMachine<'bump> { log::debug!("old_key_to_old_index, {:#?}", old_key_to_old_index); log::debug!("new_index_to_old_index, {:#?}", new_index_to_old_index); log::debug!("shared_keys, {:#?}", shared_keys); - self.replace_and_create_many_with_many(old, new); + + if let Some(first_old) = old.get(0) { + self.remove_nodes(&old[1..], true); + self.stack + .create_children(new, MountType::Replace { old: first_old }) + } else { + self.stack.create_children(new, MountType::Append {}); + } return; } @@ -1032,14 +1163,16 @@ impl<'bump> DiffMachine<'bump> { VNode::Element(t) => break t.dom_id.get(), VNode::Suspended(t) => break t.dom_id.get(), VNode::Anchor(t) => break t.dom_id.get(), - + VNode::Linked(l) => { + let node: &VNode = unsafe { std::mem::transmute(&*l.node) }; + self.find_last_element(node); + } VNode::Fragment(frag) => { search_node = frag.children.last(); } 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_node()); + search_node = Some(self.scopes.root_node(&scope_id)); } } } @@ -1056,8 +1189,11 @@ 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_node()); + search_node = Some(self.scopes.root_node(&scope_id)); + } + VNode::Linked(link) => { + let node = unsafe { std::mem::transmute(&*link.node) }; + search_node = Some(node); } VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), @@ -1067,17 +1203,38 @@ impl<'bump> DiffMachine<'bump> { } } - fn replace_and_create_many_with_one( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump VNode<'bump>, - ) { - if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - self.stack - .create_node(new, MountType::Replace { old: first_old }); - } else { - self.stack.create_node(new, MountType::Append {}); + fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { + match old { + VNode::Element(el) => { + let id = old.try_mounted_id().expect(&format!("broke on {:?}", old)); + self.mutations.replace_with(id, nodes_created as u32); + self.remove_nodes(el.children, false); + } + + VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + let id = old.try_mounted_id().expect(&format!("broke on {:?}", old)); + self.mutations.replace_with(id, nodes_created as u32); + } + + VNode::Fragment(f) => { + self.replace_node(&f.children[0], nodes_created); + self.remove_nodes(f.children.iter().skip(1), true); + } + + VNode::Component(c) => { + let node = self.scopes.fin_head(&c.associated_scope.get().unwrap()); + self.replace_node(node, nodes_created); + + let scope_id = c.associated_scope.get().unwrap(); + println!("replacing c {:?} ", scope_id); + log::debug!("Destroying scope {:?}", scope_id); + self.scopes.try_remove(&scope_id).unwrap(); + } + + VNode::Linked(l) => { + let node: &'bump VNode<'bump> = unsafe { std::mem::transmute(&*l.node) }; + self.replace_node(node, nodes_created); + } } } @@ -1092,16 +1249,18 @@ impl<'bump> DiffMachine<'bump> { for node in nodes { match node { VNode::Text(t) => { - let id = t.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + // this check exists because our null node will be removed but does not have an ID + if let Some(id) = t.dom_id.get() { + self.scopes.collect_garbage(id); - if gen_muts { - self.mutations.remove(id.as_u64()); + if gen_muts { + self.mutations.remove(id.as_u64()); + } } } VNode::Suspended(s) => { let id = s.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + self.scopes.collect_garbage(id); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1109,7 +1268,7 @@ impl<'bump> DiffMachine<'bump> { } VNode::Anchor(a) => { let id = a.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + self.scopes.collect_garbage(id); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1129,56 +1288,45 @@ impl<'bump> DiffMachine<'bump> { self.remove_nodes(f.children, gen_muts); } - VNode::Component(c) => { - let scope_id = c.associated_scope.get().unwrap(); - let scope = self.vdom.get_scope_mut(scope_id).unwrap(); - let root = scope.root_node(); - self.remove_nodes(Some(root), gen_muts); + VNode::Linked(l) => { + let node = unsafe { std::mem::transmute(&*l.node) }; + self.remove_nodes(Some(node), gen_muts); + } - log::debug!("Destroying scope {:?}", scope_id); - let mut s = self.vdom.try_remove(scope_id).unwrap(); - s.hooks.clear_hooks(); + VNode::Component(c) => { + self.destroy_vomponent(c, gen_muts); } } } } - /// Remove all the old nodes and replace them with newly created new nodes. - /// - /// The new nodes *will* be created - don't create them yourself! - fn replace_and_create_many_with_many( - &mut self, - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], - ) { - if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - self.stack - .create_children(new, MountType::Replace { old: first_old }) - } else { - self.stack.create_children(new, MountType::Append {}); - } + fn destroy_vomponent(&mut self, vc: &VComponent, gen_muts: bool) { + let scope_id = vc.associated_scope.get().unwrap(); + let root = self.scopes.root_node(&scope_id); + self.remove_nodes(Some(root), gen_muts); + log::debug!("Destroying scope {:?}", scope_id); + self.scopes.try_remove(&scope_id).unwrap(); } /// Adds a listener closure to a scope during diff. - fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &ScopeInner) { - let mut queue = scope.listeners.borrow_mut(); - let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; - queue.push(long_listener as *const _) + fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &Scope) { + let long_listener = unsafe { std::mem::transmute(listener) }; + scope.items.borrow_mut().listeners.push(long_listener) } 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)) + .and_then(|id| self.scopes.get_scope(&id)) { // safety: this lifetime is managed by the logic on scope - let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) }; + let extended = unsafe { std::mem::transmute(suspended) }; scope - .suspended_nodes + .items .borrow_mut() - .insert(suspended.task_id, extended as *const _); + .suspended_nodes + .insert(suspended.task_id, extended); } } } diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs deleted file mode 100644 index 8685824a1..000000000 --- a/packages/core/src/diff_stack.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::innerlude::*; -use smallvec::{smallvec, SmallVec}; - -/// The stack instructions we use to diff and create new nodes. -#[derive(Debug)] -pub(crate) enum DiffInstruction<'a> { - Diff { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - - Create { - node: &'a VNode<'a>, - }, - - /// pushes the node elements onto the stack for use in mount - PrepareMove { - node: &'a VNode<'a>, - }, - - Mount { - and: MountType<'a>, - }, - - PopScope, -} - -#[derive(Debug, Clone, Copy)] -pub enum MountType<'a> { - Absorb, - Append, - Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: &'a VNode<'a> }, - InsertBefore { other_node: &'a VNode<'a> }, -} - -pub(crate) struct DiffStack<'bump> { - pub(crate) instructions: Vec>, - nodes_created_stack: SmallVec<[usize; 10]>, - pub scope_stack: SmallVec<[ScopeId; 5]>, -} - -impl<'bump> DiffStack<'bump> { - pub fn new() -> Self { - Self { - instructions: Vec::with_capacity(1000), - nodes_created_stack: smallvec![], - scope_stack: smallvec![], - } - } - - pub fn is_empty(&self) -> bool { - self.instructions.is_empty() - } - - pub fn pop(&mut self) -> Option> { - self.instructions.pop() - } - - pub fn pop_off_scope(&mut self) { - self.scope_stack.pop(); - } - - pub fn push(&mut self, instruction: DiffInstruction<'bump>) { - self.instructions.push(instruction) - } - - pub fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - - for child in children.iter().rev() { - self.instructions - .push(DiffInstruction::Create { node: child }); - } - } - - pub fn push_subtree(&mut self) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { - and: MountType::Append, - }); - } - - pub fn push_nodes_created(&mut self, count: usize) { - self.nodes_created_stack.push(count); - } - - 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 }); - self.instructions.push(DiffInstruction::Create { node }); - } - - pub fn add_child_count(&mut self, count: usize) { - *self.nodes_created_stack.last_mut().unwrap() += count; - } - - pub fn pop_nodes_created(&mut self) -> usize { - self.nodes_created_stack.pop().unwrap() - } - - pub fn current_scope(&self) -> Option { - self.scope_stack.last().copied() - } - - pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { - // Push the new scope onto the stack - self.scope_stack.push(idx); - - self.instructions.push(DiffInstruction::PopScope); - - // Run the creation algorithm with this scope on the stack - // ?? I think we treat components as fragments?? - self.instructions.push(DiffInstruction::Create { node }); - } -} diff --git a/packages/core/src/heuristics.rs b/packages/core/src/heuristics.rs deleted file mode 100644 index a8069c95e..000000000 --- a/packages/core/src/heuristics.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::collections::HashMap; - -use fxhash::FxHashMap; - -use crate::FC; - -/// Provides heuristics to the "SharedResources" object for improving allocation performance. -/// -/// This heuristics engine records the memory footprint of bump arenas and hook lists for each component. These records are -/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on -/// that would slow down the diffing and initialization process. -/// -/// -pub struct HeuristicsEngine { - heuristics: FxHashMap, -} - -pub type FcSlot = *const (); - -pub struct Heuristic { - hooks: u32, - bump_size: u64, -} - -impl HeuristicsEngine { - pub(crate) fn new() -> Self { - Self { - heuristics: FxHashMap::default(), - } - } - - fn recommend(&mut self, fc: FC, heuristic: Heuristic) { - let g = fc as FcSlot; - let e = self.heuristics.entry(g); - } - - fn get_recommendation(&mut self, fc: FC) -> &Heuristic { - let id = fc as FcSlot; - - self.heuristics.entry(id).or_insert(Heuristic { - bump_size: 100, - hooks: 10, - }) - } -} - -#[test] -fn types_work() { - let engine = HeuristicsEngine::new(); -} diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs deleted file mode 100644 index ed4eb49dd..000000000 --- a/packages/core/src/hooklist.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - any::Any, - cell::{Cell, RefCell, UnsafeCell}, -}; - -type UnsafeInnerHookState = UnsafeCell>; -type HookCleanup = Box)>; - -/// 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>, - idx: Cell, -} - -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.0.get() }; - raw_box.downcast_mut::() - }) - } - - /// 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 considered unsafe - /// - /// This should only be ran by Dioxus itself before "running scope". - /// Dioxus knows how to descend through the tree to prevent mutable aliasing. - pub(crate) unsafe fn reset(&mut self) { - self.idx.set(0); - } - - pub(crate) fn push_hook(&self, new: T, cleanup: HookCleanup) { - self.vals - .borrow_mut() - .push((UnsafeCell::new(Box::new(new)), cleanup)) - } - - pub(crate) fn len(&self) -> usize { - self.vals.borrow().len() - } - - pub(crate) fn cur_idx(&self) -> usize { - self.idx.get() - } - - pub(crate) fn at_end(&self) -> bool { - self.cur_idx() >= self.len() - } - - pub fn clear_hooks(&mut self) { - log::debug!("clearing hooks..."); - self.vals - .borrow_mut() - .drain(..) - .for_each(|(state, cleanup)| cleanup(state.into_inner())); - } -} diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs deleted file mode 100644 index 1c52a3422..000000000 --- a/packages/core/src/hooks.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Built-in hooks -//! -//! This module contains all the low-level built-in hooks that require first party support to work. -//! -//! Hooks: -//! - [`use_hook`] -//! - [`use_state_provider`] -//! - [`use_state_consumer`] -//! - [`use_task`] -//! - [`use_suspense`] - -use crate::innerlude::*; -use futures_util::FutureExt; -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" -/// }); -/// -/// 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 - Out: 'static, - Fut: Future + 'static, - Init: FnOnce() -> Fut + 'src, -{ - struct TaskHook { - handle: TaskHandle, - task_dump: Rc>>, - value: Option, - } - - // whenever the task is complete, save it into th - cx.use_hook( - move |_| { - let task_fut = task_initializer(); - - let task_dump = Rc::new(RefCell::new(None)); - - let slot = task_dump.clone(); - - 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 { - *slot.as_ref().borrow_mut() = Some(output); - updater(originator); - originator - }))); - - TaskHook { - task_dump, - value: None, - handle, - } - }, - |hook| { - if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { - hook.value = Some(val); - } - (&hook.handle, &hook.value) - }, - |_| {}, - ) -} - -/// Asynchronously render new nodes once the given future has completed. -/// -/// # Easda -/// -/// -/// -/// -/// # Example -/// -/// -pub fn use_suspense<'src, Out, Fut, Cb>( - cx: Context<'src>, - task_initializer: impl FnOnce() -> Fut, - user_callback: Cb, -) -> Element<'src> -where - Fut: Future + 'static, - Out: 'static, - Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> Element<'a> + 'static, -{ - /* - General strategy: - - Create a slot for the future to dump its output into - - Create a new future feeding off the user's future that feeds the output into that slot - - Submit that future as a task - - Take the task handle id and attach that to our suspended node - - when the hook runs, check if the value exists - - if it does, then we can render the node directly - - if it doesn't, then we render a suspended node along with with the callback and task id - */ - cx.use_hook( - move |_| { - let value = Rc::new(RefCell::new(None)); - let slot = value.clone(); - let originator = cx.scope.our_arena_idx; - - let handle = cx.submit_task(Box::pin(task_initializer().then( - move |output| async move { - *slot.borrow_mut() = Some(Box::new(output) as Box); - originator - }, - ))); - - SuspenseHook { handle, value } - }, - move |hook| match hook.value.borrow().as_ref() { - Some(value) => { - let out = value.downcast_ref::().unwrap(); - let sus = SuspendedContext { - inner: Context { scope: cx.scope }, - }; - user_callback(sus, out) - } - None => { - let value = hook.value.clone(); - - let id = hook.handle.our_id; - - todo!() - // Some(LazyNodes::new(move |f| { - // let bump = f.bump(); - - // use bumpalo::boxed::Box as BumpBox; - - // let f: &mut dyn FnMut(SuspendedContext<'src>) -> Element<'src> = - // bump.alloc(move |sus| { - // let val = value.borrow(); - - // let out = val - // .as_ref() - // .unwrap() - // .as_ref() - // .downcast_ref::() - // .unwrap(); - - // user_callback(sus, out) - // }); - // let callback = unsafe { BumpBox::from_raw(f) }; - - // VNode::Suspended(bump.alloc(VSuspended { - // dom_id: empty_cell(), - // task_id: id, - // callback: RefCell::new(Some(callback)), - // })) - // })) - } - }, - |_| {}, - ) -} - -pub(crate) struct SuspenseHook { - pub handle: TaskHandle, - pub value: Rc>>>, -} - -pub struct SuspendedContext<'a> { - pub(crate) inner: Context<'a>, -} - -impl<'src> SuspendedContext<'src> { - // // pub fn render( - // pub fn render( - // // pub fn render) -> VNode<'src>>( - // self, - // lazy_nodes: LazyNodes<'_>, - // // lazy_nodes: LazyNodes<'src, '_>, - // ) -> Element<'src> { - // let bump = &self.inner.scope.frames.wip_frame().bump; - // todo!("suspense") - // // Some(lazy_nodes.into_vnode(NodeFactory { bump })) - // } -} - -#[derive(Clone, Copy)] -pub struct NodeRef<'src, T: 'static>(&'src RefCell>); - -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/lazynodes.rs b/packages/core/src/lazynodes.rs index 19f56cf58..d6bf3fda2 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -27,89 +27,107 @@ pub struct LazyNodes<'a, 'b> { inner: StackNodeStorage<'a, 'b>, } -type StackHeapSize = [usize; 0]; +type StackHeapSize = [usize; 16]; enum StackNodeStorage<'a, 'b> { Stack(LazyStack), - Heap(Box) -> VNode<'a> + 'b>), + Heap(Box>) -> Option> + 'b>), } impl<'a, 'b> LazyNodes<'a, 'b> { - pub fn new(val: F) -> Self + pub fn new(_val: F) -> Self where F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, { - unsafe { - let mut ptr: *const _ = &val as &dyn FnOnce(NodeFactory<'a>) -> VNode<'a>; + // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch + let mut slot = Some(_val); - assert_eq!( - ptr as *const u8, &val as *const _ as *const u8, - "MISUSE: Closure returned different pointer" - ); - assert_eq!( - std::mem::size_of_val(&*ptr), - std::mem::size_of::(), - "MISUSE: Closure returned a subset pointer" - ); - let words = ptr_as_slice(&mut ptr); - assert!( - words[0] == &val as *const _ as usize, - "BUG: Pointer layout is not (data_ptr, info...)" - ); + let val = move |fac: Option>| { + let inner = slot.take().unwrap(); + fac.map(inner) + }; - // - Ensure that Self is aligned same as data requires - assert!( - std::mem::align_of::() <= std::mem::align_of::(), - "TODO: Enforce alignment >{} (requires {})", - std::mem::align_of::(), - std::mem::align_of::() - ); + unsafe { LazyNodes::new_inner(val) } + } - let info = &words[1..]; - let data = words[0] as *mut (); - let size = mem::size_of::(); + unsafe fn new_inner(val: F) -> Self + where + F: FnMut(Option>) -> Option> + 'b, + { + let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; - if info.len() * mem::size_of::() + size > mem::size_of::() { - log::debug!("lazy nodes was too large to fit into stack. falling back to heap"); + assert_eq!( + ptr as *const u8, &val as *const _ as *const u8, + "MISUSE: Closure returned different pointer" + ); + assert_eq!( + std::mem::size_of_val(&*ptr), + std::mem::size_of::(), + "MISUSE: Closure returned a subset pointer" + ); - Self { - inner: StackNodeStorage::Heap(Box::new(val)), + let words = ptr_as_slice(&mut ptr); + assert!( + words[0] == &val as *const _ as usize, + "BUG: Pointer layout is not (data_ptr, info...)" + ); + + // - Ensure that Self is aligned same as data requires + assert!( + std::mem::align_of::() <= std::mem::align_of::(), + "TODO: Enforce alignment >{} (requires {})", + std::mem::align_of::(), + std::mem::align_of::() + ); + + let info = &words[1..]; + let data = words[0] as *mut (); + let size = mem::size_of::(); + + let stored_size = info.len() * mem::size_of::() + size; + let max_size = mem::size_of::(); + + if stored_size > max_size { + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } + } else { + let mut buf: StackHeapSize = StackHeapSize::default(); + + assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); + + // Place pointer information at the end of the region + // - Allows the data to be at the start for alignment purposes + { + let info_ofs = buf.as_ref().len() - info.len(); + let info_dst = &mut buf.as_mut()[info_ofs..]; + for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { + *d = *v; } - } else { - log::debug!("lazy nodes fits on stack!"); - let mut buf: StackHeapSize = StackHeapSize::default(); + } - assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); + let src_ptr = data as *const u8; + let dataptr = buf.as_mut()[..].as_mut_ptr() as *mut u8; + for i in 0..size { + *dataptr.add(i) = *src_ptr.add(i); + } - // Place pointer information at the end of the region - // - Allows the data to be at the start for alignment purposes - { - let info_ofs = buf.as_ref().len() - info.len(); - let info_dst = &mut buf.as_mut()[info_ofs..]; - for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { - *d = *v; - } - } + std::mem::forget(val); - let src_ptr = data as *const u8; - let dataptr = buf.as_mut()[..].as_mut_ptr() as *mut u8; - for i in 0..size { - *dataptr.add(i) = *src_ptr.add(i); - } - - std::mem::forget(val); - - Self { - inner: StackNodeStorage::Stack(LazyStack { _align: [], buf }), - } + Self { + inner: StackNodeStorage::Stack(LazyStack { + _align: [], + buf, + dropped: false, + }), } } } pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { match self.inner { - StackNodeStorage::Heap(lazy) => lazy(f), - StackNodeStorage::Stack(stack) => stack.call(f), + StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(), + StackNodeStorage::Stack(mut stack) => stack.call(f), } } } @@ -117,34 +135,51 @@ impl<'a, 'b> LazyNodes<'a, 'b> { struct LazyStack { _align: [u64; 0], buf: StackHeapSize, + dropped: bool, } impl LazyStack { - unsafe fn create_boxed<'a>(&mut self) -> Box) -> VNode<'a>> { + fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { let LazyStack { buf, .. } = self; let data = buf.as_ref(); - let info_size = mem::size_of::<*mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a>>() - / mem::size_of::() - - 1; + let info_size = + mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() + / mem::size_of::() + - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a> = - make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]); + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - Box::from_raw(g) - } + self.dropped = true; - fn call(mut self, f: NodeFactory) -> VNode { - let boxed = unsafe { self.create_boxed() }; - boxed(f) + let clos = unsafe { &mut *g }; + clos(Some(f)).unwrap() } } impl Drop for LazyStack { fn drop(&mut self) { - let boxed = unsafe { self.create_boxed() }; - mem::drop(boxed); + if !self.dropped { + let LazyStack { buf, .. } = self; + let data = buf.as_ref(); + + let info_size = mem::size_of::< + *mut dyn FnMut(Option>) -> Option>, + >() / mem::size_of::() + - 1; + + let info_ofs = data.len() - info_size; + + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; + + self.dropped = true; + + let clos = unsafe { &mut *g }; + clos(None); + } } } @@ -177,7 +212,7 @@ fn round_to_words(len: usize) -> usize { fn it_works() { let bump = bumpalo::Bump::new(); - simple_logger::init(); + simple_logger::init().unwrap(); let factory = NodeFactory { bump: &bump }; @@ -191,3 +226,49 @@ fn it_works() { dbg!(g); } + +#[test] +fn it_drops() { + use std::rc::Rc; + let bump = bumpalo::Bump::new(); + + simple_logger::init().unwrap(); + + // let factory = NodeFactory { scope: &bump }; + + struct DropInner { + id: i32, + } + impl Drop for DropInner { + fn drop(&mut self) { + log::debug!("dropping inner"); + } + } + let val = Rc::new(10); + + { + let it = (0..10) + .map(|i| { + let val = val.clone(); + + NodeFactory::annotate_lazy(move |f| { + log::debug!("hell closure"); + let inner = DropInner { id: i }; + f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) + }) + }) + .collect::>(); + + let caller = NodeFactory::annotate_lazy(|f| { + log::debug!("main closure"); + f.fragment_from_iter(it) + }) + .unwrap(); + } + + // let nodes = caller.call(factory); + + // dbg!(nodes); + + dbg!(Rc::strong_count(&val)); +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f602f512e..a4ea08399 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,69 +12,39 @@ Navigating this crate: Some utilities */ -pub mod bumpframe; -pub mod childiter; -pub mod component; -pub mod context; -pub mod diff; -pub mod diff_stack; -pub mod events; -pub mod heuristics; -pub mod hooklist; -pub mod hooks; -pub mod lazynodes; -pub mod mutations; -pub mod nodes; -pub mod resources; -pub mod scheduler; -pub mod scope; -pub mod tasks; -pub mod test_dom; -pub mod threadsafe; -pub mod util; -pub mod virtual_dom; - -#[cfg(feature = "debug_vdom")] -pub mod debug_dom; +pub(crate) mod component; +pub(crate) mod diff; +pub(crate) mod lazynodes; +pub(crate) mod mutations; +pub(crate) mod nodes; +pub(crate) mod scope; +pub(crate) mod scopearena; +pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::bumpframe::*; - pub(crate) use crate::childiter::*; pub use crate::component::*; - pub use crate::context::*; - pub(crate) use crate::diff::*; - pub use crate::diff_stack::*; - pub use crate::events::*; - pub use crate::heuristics::*; - pub(crate) use crate::hooklist::*; - pub use crate::hooks::*; + pub use crate::diff::*; pub use crate::lazynodes::*; 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::threadsafe::*; - pub use crate::util::*; + pub use crate::scopearena::*; pub use crate::virtual_dom::*; - pub type Element<'a> = Option>; - pub type FC

= for<'a> fn(Scope<'a, P>) -> Element<'a>; + pub type Element = Option; + pub type FC

= for<'a> fn(Context<'a>, &'a P) -> Element; } pub use crate::innerlude::{ - Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType, - Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, SuspendedContext, - TaskHandle, TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, + Attribute, Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, IntoVNode, + LazyNodes, Listener, MountType, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeId, + UserEvent, VAnchor, VElement, VFragment, VNode, VSuspended, VirtualDom, FC, }; pub mod prelude { - pub use crate::component::{fc_to_builder, Fragment, Properties, Scope}; - pub use crate::context::Context; - pub use crate::hooks::*; - pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, ScopeChildren, FC}; + pub use crate::component::{fc_to_builder, Fragment, Properties}; + pub use crate::innerlude::Context; + pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, Scope, FC}; pub use crate::nodes::VNode; pub use crate::VirtualDom; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index ab8106b77..1686b1c08 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -3,21 +3,32 @@ //! This module contains an internal API to generate these instructions. use crate::innerlude::*; -use std::any::Any; +use std::{any::Any, fmt::Debug}; -#[derive(Debug)] pub struct Mutations<'a> { pub edits: Vec>, pub noderefs: Vec>, + pub effects: Vec<&'a dyn FnMut()>, +} +impl Debug for Mutations<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Mutations") + .field("edits", &self.edits) + .field("noderefs", &self.noderefs) + // .field("effects", &self.effects) + .finish() + } } use DomEdit::*; impl<'a> Mutations<'a> { pub(crate) fn new() -> Self { - let edits = Vec::new(); - let noderefs = Vec::new(); - Self { edits, noderefs } + Self { + edits: Vec::new(), + noderefs: Vec::new(), + effects: Vec::new(), + } } // Navigation diff --git a/packages/core/src/noderef.rs b/packages/core/src/noderef.rs deleted file mode 100644 index 16db8fe0d..000000000 --- a/packages/core/src/noderef.rs +++ /dev/null @@ -1,119 +0,0 @@ -// let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - -// let mut garbage_list = scope.consume_garbage(); - -// let mut scopes_to_kill = Vec::new(); -// while let Some(node) = garbage_list.pop() { -// match &node.kind { -// VNodeKind::Text(_) => { -// self.shared.collect_garbage(node.direct_id()); -// } -// VNodeKind::Anchor(_) => { -// self.shared.collect_garbage(node.direct_id()); -// } -// VNodeKind::Suspended(_) => { -// self.shared.collect_garbage(node.direct_id()); -// } - -// VNodeKind::Element(el) => { -// self.shared.collect_garbage(node.direct_id()); -// for child in el.children { -// garbage_list.push(child); -// } -// } - -// VNodeKind::Fragment(frag) => { -// for child in frag.children { -// garbage_list.push(child); -// } -// } - -// VNodeKind::Component(comp) => { -// // TODO: run the hook destructors and then even delete the scope - -// let scope_id = comp.ass_scope.get().unwrap(); -// let scope = self.get_scope(scope_id).unwrap(); -// let root = scope.root(); -// garbage_list.push(root); -// scopes_to_kill.push(scope_id); -// } -// } -// } - -// for scope in scopes_to_kill { -// // oy kill em -// log::debug!("should be removing scope {:#?}", scope); -// } - -// // On the primary event queue, there is no batching, we take them off one-by-one -// let trigger = match receiver.try_next() { -// Ok(Some(trigger)) => trigger, -// _ => { -// // Continuously poll the future pool and the event receiver for work -// let mut tasks = self.shared.async_tasks.borrow_mut(); -// let tasks_tasks = tasks.next(); - -// // if the new event generates work more important than our current fiber, we should consider switching -// // only switch if it impacts different scopes. -// let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut(); -// let ui_reciv_task = ui_receiver.next(); - -// // right now, this polling method will only catch batched set_states that don't get awaited. -// // However, in the future, we might be interested in batching set_states across await points -// let immediate_tasks = (); - -// futures_util::pin_mut!(tasks_tasks); -// futures_util::pin_mut!(ui_reciv_task); - -// // Poll the event receiver and the future pool for work -// // Abort early if our deadline has ran out -// let mut deadline = (&mut deadline_future).fuse(); - -// let trig = futures_util::select! { -// trigger = tasks_tasks => trigger, -// trigger = ui_reciv_task => trigger, - -// // abort if we're out of time -// _ = deadline => { return Ok(diff_machine.mutations); } -// }; - -// trig.unwrap() -// } -// }; - -// async fn select_next_event(&mut self) -> Option { -// let mut receiver = self.shared.task_receiver.borrow_mut(); - -// // drain the in-flight events so that we can sort them out with the current events -// while let Ok(Some(trigger)) = receiver.try_next() { -// log::info!("retrieving event from receiver"); -// let key = self.shared.make_trigger_key(&trigger); -// self.pending_events.insert(key, trigger); -// } - -// if self.pending_events.is_empty() { -// // Continuously poll the future pool and the event receiver for work -// let mut tasks = self.shared.async_tasks.borrow_mut(); -// let tasks_tasks = tasks.next(); - -// let mut receiver = self.shared.task_receiver.borrow_mut(); -// let reciv_task = receiver.next(); - -// futures_util::pin_mut!(tasks_tasks); -// futures_util::pin_mut!(reciv_task); - -// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { -// futures_util::future::Either::Left((trigger, _)) => trigger, -// futures_util::future::Either::Right((trigger, _)) => trigger, -// } -// .unwrap(); -// let key = self.shared.make_trigger_key(&trigger); -// self.pending_events.insert(key, trigger); -// } - -// // pop the most important event off -// let key = self.pending_events.keys().next().unwrap().clone(); -// let trigger = self.pending_events.remove(&key).unwrap(); - -// Some(trigger) -// } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 9b9693a8e..62c3ab837 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,14 +4,12 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{ - empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner, - SuspendedContext, - }, + innerlude::{Context, Element, Properties, Scope, ScopeId}, lazynodes::LazyNodes, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ + any::Any, cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, }; @@ -19,8 +17,8 @@ use std::{ /// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. /// /// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of: +/// /// - the [`rsx`] macro -/// - the [`html`] macro /// - the [`NodeFactory`] API pub enum VNode<'src> { /// Text VNodes simply bump-allocated (or static) string slices @@ -28,7 +26,8 @@ pub enum VNode<'src> { /// # Example /// /// ``` - /// let node = cx.render(rsx!{ "hello" }).unwrap(); + /// let mut vdom = VirtualDom::new(); + /// let node = vdom.render_vnode(rsx!( "hello" )); /// /// if let VNode::Text(vtext) = node { /// assert_eq!(vtext.text, "hello"); @@ -43,7 +42,9 @@ pub enum VNode<'src> { /// # Example /// /// ```rust - /// let node = cx.render(rsx!{ + /// let mut vdom = VirtualDom::new(); + /// + /// let node = vdom.render_vnode(rsx!{ /// div { /// key: "a", /// onclick: |e| log::info!("clicked"), @@ -51,7 +52,8 @@ pub enum VNode<'src> { /// style: { background_color: "red" } /// "hello" /// } - /// }).unwrap(); + /// }); + /// /// if let VNode::Element(velement) = node { /// assert_eq!(velement.tag_name, "div"); /// assert_eq!(velement.namespace, None); @@ -80,13 +82,13 @@ pub enum VNode<'src> { /// # Example /// /// ```rust - /// fn Example(cx: Context<()>) -> DomTree { + /// fn Example(cx: Context, props: &()) -> Element { /// todo!() /// } /// - /// let node = cx.render(rsx!{ - /// Example {} - /// }).unwrap(); + /// let mut vdom = VirtualDom::new(); + /// + /// let node = vdom.render_vnode(rsx!( Example {} )); /// /// if let VNode::Component(vcomp) = node { /// assert_eq!(vcomp.user_fc, Example as *const ()); @@ -96,13 +98,11 @@ pub enum VNode<'src> { /// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed. /// - /// These nodes currently can only be constructed via the [`use_suspense`] hook. - /// /// # Example /// /// ```rust - /// rsx!{ - /// } + /// + /// /// ``` Suspended(&'src VSuspended<'src>), @@ -113,13 +113,32 @@ pub enum VNode<'src> { /// # Example /// /// ```rust - /// let node = cx.render(rsx! ( Fragment {} )).unwrap(); + /// let mut vdom = VirtualDom::new(); + /// + /// let node = vdom.render_vnode(rsx!( Fragment {} )); + /// /// if let VNode::Fragment(frag) = node { /// let root = &frag.children[0]; /// assert_eq!(root, VNode::Anchor); /// } /// ``` Anchor(&'src VAnchor), + + /// A VNode that is actually a pointer to some nodes rather than the nodes directly. Useful when rendering portals + /// or eliding lifetimes on VNodes through runtime checks. + /// + /// Linked VNodes can only be made through the [`Context::render`] method + /// + /// Typically, linked nodes are found *not* in a VNode. When NodeLinks are in a VNode, the NodeLink was passed into + /// an `rsx!` call. + /// + /// # Example + /// ```rust + /// let mut vdom = VirtualDom::new(); + /// + /// let node: NodeLink = vdom.render_vnode(rsx!( "hello" )); + /// ``` + Linked(NodeLink), } impl<'src> VNode<'src> { @@ -129,9 +148,11 @@ impl<'src> VNode<'src> { VNode::Element(el) => el.key, VNode::Component(c) => c.key, VNode::Fragment(f) => f.key, + VNode::Text(_t) => None, VNode::Suspended(_s) => None, VNode::Anchor(_f) => None, + VNode::Linked(_c) => None, } } @@ -151,11 +172,21 @@ impl<'src> VNode<'src> { VNode::Element(el) => el.dom_id.get(), VNode::Anchor(el) => el.dom_id.get(), VNode::Suspended(el) => el.dom_id.get(), + + VNode::Linked(_) => None, VNode::Fragment(_) => None, VNode::Component(_) => None, } } + pub(crate) fn children(&self) -> &[VNode<'src>] { + match &self { + VNode::Fragment(f) => f.children, + VNode::Component(_c) => todo!("children are not accessible through this"), + _ => &[], + } + } + // Create an "owned" version of the vnode. pub fn decouple(&self) -> VNode<'src> { match self { @@ -167,7 +198,11 @@ impl<'src> VNode<'src> { VNode::Fragment(f) => VNode::Fragment(VFragment { children: f.children, key: f.key, - is_static: f.is_static, + }), + VNode::Linked(c) => VNode::Linked(NodeLink { + scope_id: c.scope_id.clone(), + link_idx: c.link_idx.clone(), + node: c.node.clone(), }), } } @@ -177,21 +212,46 @@ 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") + .debug_struct("VNode::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"), + VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), + VNode::Anchor(_) => write!(s, "VNode::VAnchor"), - VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children), - VNode::Suspended { .. } => write!(s, "VSuspended"), - VNode::Component(comp) => write!(s, "VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Fragment(frag) => { + write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) + } + VNode::Suspended { .. } => write!(s, "VNode::VSuspended"), + VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Linked(c) => write!(s, "VNode::VCached {{ scope_id: {:?} }}", c.scope_id.get()), } } } +/// 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 std::fmt::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 + } +} + +fn empty_cell() -> Cell> { + Cell::new(None) +} + /// A placeholder node only generated when Fragments don't have any children. pub struct VAnchor { pub dom_id: Cell>, @@ -209,10 +269,7 @@ pub struct VText<'src> { /// A list of VNodes with no single root. pub struct VFragment<'src> { pub key: Option<&'src str>, - pub children: &'src [VNode<'src>], - - pub is_static: bool, } /// An element like a "div" with children, listeners, and attributes. @@ -307,12 +364,11 @@ pub struct Listener<'bump> { /// IE "click" - whatever the renderer needs to attach the listener by name. pub event: &'static str, - #[allow(clippy::type_complexity)] /// The actual callback that the user specified - pub(crate) callback: - RefCell) + 'bump>>>, + pub(crate) callback: RefCell) + 'bump>>>, } +pub type VCompCaller<'src> = BumpBox<'src, dyn Fn(Context) -> Element + 'src>; /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { @@ -320,21 +376,23 @@ pub struct VComponent<'src> { pub associated_scope: Cell>, - pub is_static: bool, - // Function pointer to the FC that was used to generate this component pub user_fc: *const (), - pub(crate) caller: &'src dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, + pub(crate) can_memoize: bool, + + pub(crate) hard_allocation: Cell>, + + // Raw pointer into the bump arena for the props of the component + pub(crate) bump_props: *const (), + + // during the "teardown" process we'll take the caller out so it can be dropped properly + // pub(crate) caller: Option>, + pub(crate) caller: &'src dyn Fn(&'src Scope) -> Element, pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>, pub(crate) drop_props: RefCell>>, - - pub(crate) can_memoize: bool, - - // Raw pointer into the bump arena for the props of the component - pub(crate) raw_props: *const (), } pub struct VSuspended<'a> { @@ -342,7 +400,41 @@ pub struct VSuspended<'a> { pub dom_id: Cell>, #[allow(clippy::type_complexity)] - pub callback: RefCell) -> Element<'a>>>>, + pub callback: RefCell Element + 'a>>>, +} + +/// A cached node is a "pointer" to a "rendered" node in a particular scope +/// +/// It does not provide direct access to the node, so it doesn't carry any lifetime information with it +/// +/// It is used during the diffing/rendering process as a runtime key into an existing set of nodes. The "render" key +/// is essentially a unique key to guarantee safe usage of the Node. +/// +/// Linked VNodes can only be made through the [`Context::render`] method +/// +/// Typically, NodeLinks are found *not* in a VNode. When NodeLinks are in a VNode, the NodeLink was passed into +/// an `rsx!` call. +#[derive(Debug)] +pub struct NodeLink { + pub(crate) link_idx: Cell, + pub(crate) scope_id: Cell>, + pub(crate) node: *const VNode<'static>, +} + +impl PartialEq for NodeLink { + fn eq(&self, other: &Self) -> bool { + self.node == other.node + } +} +impl NodeLink { + // we don't want to let users clone NodeLinks + pub(crate) fn clone_inner(&self) -> Self { + Self { + link_idx: self.link_idx.clone(), + scope_id: self.scope_id.clone(), + node: self.node.clone(), + } + } } /// This struct provides an ergonomic API to quickly build VNodes. @@ -361,7 +453,7 @@ impl<'a> NodeFactory<'a> { #[inline] pub fn bump(&self) -> &'a bumpalo::Bump { - self.bump + &self.bump } /// Directly pass in text blocks without the need to use the format_args macro. @@ -381,7 +473,7 @@ impl<'a> NodeFactory<'a> { Some(static_str) => (static_str, true), None => { use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(self.bump()); + let mut str_buf = bumpalo::collections::String::new_in(self.bump); str_buf.write_fmt(args).unwrap(); (str_buf.into_bump_str(), false) } @@ -437,18 +529,18 @@ impl<'a> NodeFactory<'a> { A: 'a + AsRef<[Attribute<'a>]>, V: 'a + AsRef<[VNode<'a>]>, { - let listeners: &'a L = self.bump().alloc(listeners); + let listeners: &'a L = self.bump.alloc(listeners); let listeners = listeners.as_ref(); - let attributes: &'a A = self.bump().alloc(attributes); + let attributes: &'a A = self.bump.alloc(attributes); let attributes = attributes.as_ref(); - let children: &'a V = self.bump().alloc(children); + let children: &'a V = self.bump.alloc(children); let children = children.as_ref(); let key = key.map(|f| self.raw_text(f).0); - VNode::Element(self.bump().alloc(VElement { + VNode::Element(self.bump.alloc(VElement { tag_name, key, namespace, @@ -479,19 +571,19 @@ impl<'a> NodeFactory<'a> { pub fn component

( &self, - component: fn(Scope<'a, P>) -> Element<'a>, + component: fn(Context<'a>, &'a P) -> Element, props: P, key: Option, ) -> VNode<'a> where P: Properties + 'a, { - let bump = self.bump(); + let bump = self.bump; let props = bump.alloc(props); - let raw_props = props as *mut P as *mut (); + let bump_props = props as *mut P as *mut (); let user_fc = component as *const (); - let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| { + let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| { move |other: &VComponent| { if user_fc == other.user_fc { // Safety @@ -500,12 +592,10 @@ impl<'a> NodeFactory<'a> { // - Non-static P are autoderived to memoize as false // - 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); + let real_other: &P = &*(other.bump_props as *const _ as *const P); props.memoize(real_other) }; - log::debug!("comparing props..."); - // 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 props_memoized @@ -513,7 +603,7 @@ impl<'a> NodeFactory<'a> { false } } - })); + }); let drop_props = { // create a closure to drop the props @@ -521,9 +611,8 @@ impl<'a> NodeFactory<'a> { let drop_props: &mut dyn FnMut() = bump.alloc_with(|| { move || unsafe { - log::debug!("dropping props!"); if !has_dropped { - let real_other = raw_props as *mut _ as *mut P; + let real_other = bump_props as *mut _ as *mut P; let b = BumpBox::from_raw(real_other); std::mem::drop(b); @@ -541,36 +630,45 @@ impl<'a> NodeFactory<'a> { let key = key.map(|f| self.raw_text(f).0); - let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = - bump.alloc(move |scope: &ScopeInner| -> Element { - log::debug!("calling component renderr {:?}", scope.our_arena_idx); - let props: &'_ P = unsafe { &*(raw_props as *const P) }; - - let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) }; - let s: Scope<'a, P> = (Context { scope: scp }, props); - - let res: Element = component(s); - unsafe { std::mem::transmute(res) } + let caller: &'a mut dyn Fn(&'a Scope) -> Element = + bump.alloc(move |scope: &Scope| -> Element { + let props: &'_ P = unsafe { &*(bump_props as *const P) }; + let res = component(scope, props); + res }); + let can_memoize = P::IS_STATIC; + VNode::Component(bump.alloc(VComponent { user_fc, - comparator, - raw_props, + comparator: Some(comparator), + bump_props, caller, - is_static: P::IS_STATIC, key, - can_memoize: P::IS_STATIC, + can_memoize, drop_props, associated_scope: Cell::new(None), + hard_allocation: Cell::new(None), })) } + pub fn listener( + self, + event: &'static str, + callback: BumpBox<'a, dyn FnMut(Box) + 'a>, + ) -> Listener<'a> { + Listener { + mounted_node: Cell::new(None), + event, + callback: RefCell::new(Some(callback)), + } + } + pub fn fragment_from_iter( self, node_iter: impl IntoIterator>, ) -> VNode<'a> { - let bump = self.bump(); + let bump = self.bump; let mut nodes = bumpalo::collections::Vec::new_in(bump); for node in node_iter { @@ -607,14 +705,64 @@ impl<'a> NodeFactory<'a> { VNode::Fragment(VFragment { children, key: None, - is_static: false, }) } - pub fn annotate_lazy<'z, 'b, F>(f: F) -> Option> - where - F: FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b, - { + // this isn't quite feasible yet + // I think we need some form of interior mutability or state on nodefactory that stores which subtree was created + pub fn create_children( + self, + node_iter: impl IntoIterator>, + ) -> Element { + let bump = self.bump; + let mut nodes = bumpalo::collections::Vec::new_in(bump); + + for node in node_iter { + nodes.push(node.into_vnode(self)); + } + + if nodes.is_empty() { + nodes.push(VNode::Anchor(bump.alloc(VAnchor { + dom_id: empty_cell(), + }))); + } + + let children = nodes.into_bump_slice(); + + // TODO + // We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning + // + // if cfg!(debug_assertions) { + // if children.len() > 1 { + // if children.last().unwrap().key().is_none() { + // log::error!( + // r#" + // Warning: Each child in an array or iterator should have a unique "key" prop. + // Not providing a key will lead to poor performance with lists. + // See docs.rs/dioxus for more information. + // --- + // To help you identify where this error is coming from, we've generated a backtrace. + // "#, + // ); + // } + // } + // } + + let frag = VNode::Fragment(VFragment { + children, + key: None, + }); + let ptr = self.bump.alloc(frag) as *const _; + Some(NodeLink { + link_idx: Default::default(), + scope_id: Default::default(), + node: unsafe { std::mem::transmute(ptr) }, + }) + } + + pub fn annotate_lazy<'z, 'b>( + f: impl FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b, + ) -> Option> { Some(LazyNodes::new(f)) } } @@ -687,7 +835,6 @@ impl<'a> IntoVNode<'a> for Option> { Some(lazy) => lazy.call(cx), None => VNode::Fragment(VFragment { children: &[], - is_static: false, key: None, }), } @@ -712,79 +859,48 @@ impl IntoVNode<'_> for Arguments<'_> { } } -/// Access the children elements passed into the component -/// -/// This enables patterns where a component is passed children from its parent. -/// -/// ## Details -/// -/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions -/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure -/// on the props that takes Context. -/// -/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other -/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's -/// props are valid for the static lifetime. -/// -/// ## Example -/// -/// ```rust -/// const App: FC<()> = |(cx, props)|{ -/// cx.render(rsx!{ -/// CustomCard { -/// h1 {} -/// p {} -/// } -/// }) -/// } -/// -/// const CustomCard: FC<()> = |(cx, props)|{ -/// cx.render(rsx!{ -/// div { -/// h1 {"Title card"} -/// {props.children} -/// } -/// }) -/// } -/// ``` -/// -/// ## Notes: -/// -/// This method returns a "ScopeChildren" object. This object is copy-able and preserve the correct lifetime. -pub struct ScopeChildren<'a> { - root: Option>, -} - -impl Default for ScopeChildren<'_> { - fn default() -> Self { - Self { root: None } - } -} - -impl<'a> ScopeChildren<'a> { - pub fn new(root: VNode<'a>) -> Self { - Self { root: Some(root) } - } - pub fn new_option(root: Option>) -> Self { - Self { root } - } -} - -impl IntoIterator for &ScopeChildren<'_> { - type Item = Self; - - type IntoIter = std::iter::Once; - - fn into_iter(self) -> Self::IntoIter { - std::iter::once(self) - } -} - -impl<'a> IntoVNode<'a> for &ScopeChildren<'a> { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { - match &self.root { - Some(n) => n.decouple(), - None => cx.fragment_from_iter(None as Option), +// called cx.render from a helper function +impl IntoVNode<'_> for Option { + fn into_vnode(self, _cx: NodeFactory) -> VNode { + match self { + Some(node) => VNode::Linked(node), + None => { + todo!() + } } } } + +// essentially passing elements through props +// just build a new element in place +impl IntoVNode<'_> for &Option { + fn into_vnode(self, _cx: NodeFactory) -> VNode { + match self { + Some(node) => VNode::Linked(NodeLink { + link_idx: node.link_idx.clone(), + scope_id: node.scope_id.clone(), + node: node.node, + }), + None => { + // + todo!() + } + } + } +} + +impl IntoVNode<'_> for NodeLink { + fn into_vnode(self, _cx: NodeFactory) -> VNode { + VNode::Linked(self) + } +} + +impl IntoVNode<'_> for &NodeLink { + fn into_vnode(self, _cx: NodeFactory) -> VNode { + VNode::Linked(NodeLink { + link_idx: self.link_idx.clone(), + scope_id: self.scope_id.clone(), + node: self.node, + }) + } +} diff --git a/packages/core/src/resources.rs b/packages/core/src/resources.rs deleted file mode 100644 index 4163e30d3..000000000 --- a/packages/core/src/resources.rs +++ /dev/null @@ -1,77 +0,0 @@ -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 contents 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<&ScopeInner> { - 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 ScopeInner> { - let inner = unsafe { &mut *self.components.get() }; - inner.get_mut(idx.0) - } - - 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) { - let els = unsafe { &mut *self.raw_elements.get() }; - els.remove(id.0); - } - - pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> ScopeInner) -> ScopeId { - let g = unsafe { &mut *self.components.get() }; - let entry = g.vacant_entry(); - let id = ScopeId(entry.key()); - entry.insert(f(id)); - id - } -} diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs deleted file mode 100644 index fb0ee8f69..000000000 --- a/packages/core/src/scheduler.rs +++ /dev/null @@ -1,609 +0,0 @@ -/* -Welcome to Dioxus's cooperative, priority-based scheduler. - -I hope you enjoy your stay. - -Some essential reading: -- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200 -- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440 -- https://github.com/WICG/is-input-pending -- https://web.dev/rail/ -- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react - -# What's going on? - -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 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). -The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many -different types of work simultaneously. - -# How does it work? - -Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked. -React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of -React, and the UI still stays snappy! - -While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do -this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser -has time to paint and run animations. When the browser is finished, we can step in and continue our work. - -React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not -necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially -"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause -our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields -so Rust's async machinery can take over, allowing us to customize when exactly to pause it. - -React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For -now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes -before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes -are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine -future is paused and saved (self-referentially). - -# Priority System - -So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that -doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where -priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as -a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work. - -React's priority system is quite complex. - -There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only -uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on. - -For Dioxus, we're going with a 4 tier priority system: -- Sync: Things that need to be done by the next frame, like TextInput on controlled elements -- High: for events that block all others - clicks, keyboard, and hovers -- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events) -- Low: set_state called asynchronously, and anything generated by suspense - -In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed -work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave -as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to -do anything too arduous from onInput. - -For the rest, we defer to the rIC period and work down each queue from high to low. -*/ -use crate::heuristics::*; -use crate::innerlude::*; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt}; -use fxhash::FxHashMap; -use fxhash::FxHashSet; -use indexmap::IndexSet; -use slab::Slab; -use std::{ - any::{Any, TypeId}, - cell::{Cell, UnsafeCell}, - collections::{HashSet, VecDeque}, - rc::Rc, -}; - -#[derive(Clone)] -pub(crate) struct EventChannel { - pub task_counter: Rc>, - pub cur_subtree: Rc>, - pub sender: UnboundedSender, - pub schedule_any_immediate: Rc, - pub submit_task: Rc TaskHandle>, - pub get_shared_context: GetSharedContext, -} - -pub type GetSharedContext = Rc Option>>; - -pub enum SchedulerMsg { - // events from the host - UiEvent(UserEvent), - - // setstate - Immediate(ScopeId), - - // tasks - Task(TaskMsg), -} - -pub enum TaskMsg { - ToggleTask(u64), - PauseTask(u64), - ResumeTask(u64), - DropTask(u64), -} - -/// The scheduler holds basically everything around "working" -/// -/// 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 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 -/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority -/// task shift to a higher priority task that needs mutable access to the same scopes. -/// -/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. -/// -/// -pub(crate) struct Scheduler { - /// All mounted components are arena allocated to make additions, removals, and references easy to work with - /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena. - /// - /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas - /// and rusts's guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub pool: ResourcePool, - - pub heuristics: HeuristicsEngine, - - pub receiver: UnboundedReceiver, - - // Garbage stored - pub pending_garbage: FxHashSet, - - // In-flight futures - pub async_tasks: FuturesUnordered, - - // // 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 dirty_scopes: IndexSet, - pub saved_state: Option>, - pub in_progress: bool, -} - -impl Scheduler { - pub(crate) fn new( - sender: UnboundedSender, - receiver: UnboundedReceiver, - ) -> Self { - /* - Preallocate 2000 elements and 100 scopes to avoid dynamic allocation. - Perhaps this should be configurable from some external config? - */ - let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); - let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000))); - - let heuristics = HeuristicsEngine::new(); - - let task_counter = Rc::new(Cell::new(0)); - let cur_subtree = Rc::new(Cell::new(0)); - - let channel = EventChannel { - cur_subtree, - task_counter: task_counter.clone(), - sender: sender.clone(), - schedule_any_immediate: { - let sender = sender.clone(); - Rc::new(move |id| { - // - log::debug!("scheduling immediate! {:?}", id); - sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap() - }) - }, - // todo: we want to get the futures out of the scheduler message - // the scheduler message should be send/sync - submit_task: { - Rc::new(move |fiber_task| { - let task_id = task_counter.get(); - task_counter.set(task_id + 1); - - todo!(); - // sender - // .unbounded_send(SchedulerMsg::Task(TaskMsg::SubmitTask( - // fiber_task, task_id, - // ))) - // .unwrap(); - TaskHandle { - our_id: task_id, - sender: sender.clone(), - } - }) - }, - get_shared_context: { - let components = components.clone(); - Rc::new(move |id, ty| { - let components = unsafe { &*components.get() }; - let mut search: Option<&ScopeInner> = components.get(id.0); - while let Some(inner) = search.take() { - if let Some(shared) = inner.shared_contexts.borrow().get(&ty) { - return Some(shared.clone()); - } else { - search = inner.parent_idx.map(|id| components.get(id.0)).flatten(); - } - } - None - }) - }, - }; - - let pool = ResourcePool { - 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, - - pending_garbage: FxHashSet::default(), - - heuristics, - - ui_events: VecDeque::new(), - - pending_immediates: VecDeque::new(), - - pending_tasks: VecDeque::new(), - - batched_events: VecDeque::new(), - - garbage_scopes: HashSet::new(), - - dirty_scopes: Default::default(), - saved_state: Some(saved_state), - in_progress: false, - } - } - - // returns true if the event is discrete - pub fn handle_ui_event(&mut self, event: UserEvent) -> bool { - 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) - } - } - } - - // 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 { - !(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 save_work(&mut self, lane: SavedDiffWork) { - let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) }; - self.saved_state = Some(saved); - } - - unsafe fn load_work(&mut self) -> SavedDiffWork<'static> { - self.saved_state.take().unwrap().extend() - } - pub fn handle_task(&mut self, evt: TaskMsg) { - // - } - - 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; - } - } - } - - /// Load the current lane, and work on it, periodically checking in if the deadline has been reached. - /// - /// 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() }; - - // 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) }; - - let mut ran_scopes = FxHashSet::default(); - - if machine.stack.is_empty() { - let shared = self.pool.clone(); - - self.dirty_scopes - .retain(|id| shared.get_scope(*id).is_some()); - 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).reverse() - }); - - if let Some(scopeid) = self.dirty_scopes.pop() { - log::info!("handling dirty scope {:?}", scopeid); - if !ran_scopes.contains(&scopeid) { - ran_scopes.insert(scopeid); - log::debug!("about to run scope {:?}", scopeid); - - if let Some(component) = self.pool.get_scope_mut(scopeid) { - 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 }); - } - } - } - } - } - - let work_completed = machine.work(deadline_reached); - - // 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 - } - } - - /// The primary workhorse of the VirtualDOM. - /// - /// Uses some fairly complex logic to schedule what work should be produced. - /// - /// Returns a list of successful mutations. - pub fn work_with_deadline<'a>( - &'a mut self, - mut deadline: impl FnMut() -> bool, - ) -> Vec> { - /* - Strategy: - - When called, check for any UI events that might've been received since the last frame. - - Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. - - - If there are any pending discrete events, then elevate our priority level. If our priority level is already "high," - then we need to finish the high priority work first. If the current work is "low" then analyze what scopes - will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need - to bump the other work out of the way, or choose to process it so we don't have any conflicts. - 'static components have a leg up here since their work can be re-used among multiple scopes. - "High priority" is only for blocking! Should only be used on "clicks" - - - 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? - - should we batch? - - react says no - they are continuous - - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? - */ - let mut committed_mutations = Vec::>::new(); - - while self.has_any_work() { - while let Ok(Some(msg)) = self.receiver.try_next() { - match msg { - SchedulerMsg::Task(t) => todo!(), - SchedulerMsg::Immediate(im) => { - self.dirty_scopes.insert(im); - } - SchedulerMsg::UiEvent(evt) => { - self.ui_events.push_back(evt); - } - } - } - - // 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); - - // TODO: bubble properly here - scope.call_listener(event, element); - - 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!(), - } - } - } - } - } - - let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations); - - if !work_complete { - return committed_mutations; - } - } - - committed_mutations - } - - /// Work the scheduler down, not polling any ongoing tasks. - /// - /// 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(); - - 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 diff_machine = DiffMachine::new(Mutations::new(), &mut shared); - - // TODO: drain any in-flight work - let cur_component = self - .pool - .get_scope_mut(base_scope) - .expect("The base scope should never be moved"); - - log::debug!("rebuild {:?}", base_scope); - - // 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); - - diff_machine.stack.scope_stack.push(base_scope); - - diff_machine.work(|| false); - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run successfully 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) } - } - - 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"); - - log::debug!("hard diff {:?}", base_scope); - - 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() - } - } -} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index ec5f5ad33..5176b0fba 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,14 +1,38 @@ use crate::innerlude::*; + +use futures_channel::mpsc::UnboundedSender; use fxhash::FxHashMap; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, collections::HashMap, future::Future, - pin::Pin, rc::Rc, }; +use bumpalo::{boxed::Box as BumpBox, Bump}; + +/// Components in Dioxus use the "Context" object to interact with their lifecycle. +/// +/// This lets components access props, schedule updates, integrate hooks, and expose shared state. +/// +/// For the most part, the only method you should be using regularly is `render`. +/// +/// ## Example +/// +/// ```ignore +/// #[derive(Props)] +/// struct ExampleProps { +/// name: String +/// } +/// +/// fn Example(cx: Context, props: &ExampleProps) -> Element { +/// cx.render(rsx!{ div {"Hello, {props.name}"} }) +/// } +/// ``` +pub type Context<'a> = &'a Scope; + /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -18,17 +42,28 @@ use std::{ /// /// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever /// use case they might have. -pub struct ScopeInner { - // Book-keeping about our spot in the arena - pub(crate) parent_idx: Option, +pub struct Scope { + // safety: + // + // pointers to scopes are *always* valid since they are bump allocated and never freed until this scope is also freed + // this is just a bit of a hack to not need an Rc to the ScopeArena. + // todo: replace this will ScopeId and provide a connection to scope arena directly + pub(crate) parent_scope: Option<*mut Scope>, + pub(crate) our_arena_idx: ScopeId, + pub(crate) height: u32, + pub(crate) subtree: Cell, + pub(crate) is_subtree_root: Cell, - // Nodes - pub(crate) frames: ActiveFrame, - pub(crate) caller: *const dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, + pub(crate) generation: Cell, + + // The double-buffering situation that we will use + pub(crate) frames: [BumpFrame; 2], + + pub(crate) caller: *const dyn Fn(&Scope) -> Element, /* we care about: @@ -36,9 +71,7 @@ pub struct ScopeInner { - 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) items: RefCell>, // State pub(crate) hooks: HookList, @@ -46,34 +79,27 @@ pub struct ScopeInner { // todo: move this into a centralized place - is more memory efficient pub(crate) shared_contexts: RefCell>>, - // 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, + pub(crate) sender: UnboundedSender, } -/// Public interface for Scopes. -impl ScopeInner { - /// 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() - } +pub struct SelfReferentialItems<'a> { + pub(crate) listeners: Vec<&'a Listener<'a>>, + pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, + pub(crate) suspended_nodes: FxHashMap>, + pub(crate) tasks: Vec>>, + pub(crate) pending_effects: Vec>, +} +/// 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. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(pub usize); + +// Public methods exposed to libraries and components +impl Scope { /// Get the subtree ID that this scope belongs to. /// /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route @@ -83,7 +109,7 @@ impl ScopeInner { /// # Example /// /// ```rust - /// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} })); + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// dom.rebuild(); /// /// let base = dom.base_scope(); @@ -94,16 +120,6 @@ impl ScopeInner { self.subtree.get() } - pub(crate) fn new_subtree(&self) -> Option { - if self.is_subtree_root.get() { - None - } else { - let cur = self.shared.cur_subtree.get(); - self.shared.cur_subtree.set(cur + 1); - Some(cur) - } - } - /// 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. @@ -111,7 +127,7 @@ impl ScopeInner { /// # Example /// /// ```rust - /// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} })); + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// dom.rebuild(); /// /// let base = dom.base_scope(); @@ -131,7 +147,7 @@ impl ScopeInner { /// # Example /// /// ```rust - /// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} })); + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// dom.rebuild(); /// /// let base = dom.base_scope(); @@ -139,7 +155,10 @@ impl ScopeInner { /// assert_eq!(base.parent(), None); /// ``` pub fn parent(&self) -> Option { - self.parent_idx + match self.parent_scope { + Some(p) => Some(unsafe { &*p }.our_arena_idx), + None => None, + } } /// Get the ID of this Scope within this Dioxus VirtualDOM. @@ -149,7 +168,7 @@ impl ScopeInner { /// # Example /// /// ```rust - /// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} })); + /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} })); /// dom.rebuild(); /// let base = dom.base_scope(); /// @@ -158,116 +177,290 @@ impl ScopeInner { 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>>; + /// 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 { + // pub fn schedule_update(&self) -> Rc { + let chan = self.sender.clone(); + let id = self.scope_id(); + Rc::new(move || { + let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) + } -/// Private interface for Scopes. -impl ScopeInner { - // 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. - // To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies. - // This should never happen, but is a good check to keep around - // - // Scopes cannot be made anywhere else except for this file - // Therefore, their lifetimes are connected exclusively to the virtual dom - pub(crate) fn new( - caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, - our_arena_idx: ScopeId, - parent_idx: Option, - height: u32, - subtree: u32, - shared: EventChannel, - ) -> Self { - let schedule_any_update = shared.schedule_any_immediate.clone(); + /// 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 { + let chan = self.sender.clone(); + Rc::new(move |id| { + let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) + } - let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); + /// 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 needs_update(&self) { + self.needs_update_any(self.scope_id()) + } - let caller = caller as *const _; + /// 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 needs_update_any(&self, id: ScopeId) { + let _ = self.sender.unbounded_send(SchedulerMsg::Immediate(id)); + } - // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph - let caller = unsafe { std::mem::transmute(caller) }; + /// 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 bump(&self) -> &Bump { + &self.wip_frame().bump + } - Self { - memoized_updater, - shared, - caller, - parent_idx, - our_arena_idx, - height, - subtree: Cell::new(subtree), - is_subtree_root: Cell::new(false), + /// 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. + /// + /// ## Example + /// + /// ```ignore + /// fn Component(cx: Scope, props: &Props) -> Element { + /// // Lazy assemble the VNode tree + /// let lazy_nodes = rsx!("hello world"); + /// + /// // Actually build the tree and allocate it + /// cx.render(lazy_tree) + /// } + ///``` + pub fn render<'src>(&'src self, lazy_nodes: Option>) -> Option { + let frame = self.wip_frame(); + let bump = &frame.bump; + let factory = NodeFactory { bump }; + let node = lazy_nodes.map(|f| f.call(factory))?; + let node = bump.alloc(node); - frames: ActiveFrame::new(), - hooks: Default::default(), - suspended_nodes: Default::default(), - shared_contexts: Default::default(), - listeners: Default::default(), - borrowed_props: Default::default(), + let node_ptr = node as *mut _; + let node_ptr = unsafe { std::mem::transmute(node_ptr) }; + + let link = NodeLink { + scope_id: Cell::new(Some(self.our_arena_idx)), + link_idx: Cell::new(0), + node: node_ptr, + }; + + Some(link) + } + + /// Push an effect to be ran after the component has been successfully mounted to the dom + /// Returns the effect's position in the stack + pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize { + // this is some tricker to get around not being able to actually call fnonces + let mut slot = Some(effect); + let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()()); + + // wrap it in a type that will actually drop the contents + let boxed_fut = unsafe { BumpBox::from_raw(fut) }; + + // erase the 'src lifetime for self-referential storage + let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; + + let mut items = self.items.borrow_mut(); + items.pending_effects.push(self_ref_fut); + items.pending_effects.len() - 1 + } + + /// Pushes the future onto the poll queue to be polled + /// The future is forcibly dropped if the component is not ready by the next render + pub fn push_task<'src>(&'src self, fut: impl Future + 'src) -> usize { + // allocate the future + let fut: &mut dyn Future = self.bump().alloc(fut); + + // wrap it in a type that will actually drop the contents + let boxed_fut: BumpBox> = unsafe { BumpBox::from_raw(fut) }; + + // erase the 'src lifetime for self-referential storage + let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; + + let mut items = self.items.borrow_mut(); + items.tasks.push(self_ref_fut); + items.tasks.len() - 1 + } + + /// This method enables the ability to expose state to children further down the VirtualDOM Tree. + /// + /// This is a "fundamental" operation and should only be called during initialization of a hook. + /// + /// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead. + /// + /// 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_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {}); + /// rsx!(cx, Child {}) + /// } + /// + /// static Child: FC<()> = |cx, props|{ + /// let state = cx.consume_state::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn provide_state(&self, value: T) { + self.shared_contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(value)) + .map(|f| f.downcast::().ok()) + .flatten(); + } + + /// Try to retrieve a SharedState with type T from the any parent Scope. + pub fn consume_state(&self) -> Option> { + if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { + Some(shared.clone().downcast::().unwrap()) + } else { + let mut search_parent = self.parent_scope; + + while let Some(parent_ptr) = search_parent { + let parent = unsafe { &*parent_ptr }; + if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { + return Some(shared.clone().downcast::().unwrap()); + } + search_parent = parent.parent_scope; + } + None } } - pub(crate) fn update_scope_dependencies( - &mut self, - caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, - ) { - log::debug!("Updating scope dependencies {:?}", self.our_arena_idx); - let caller = caller as *const _; - self.caller = unsafe { std::mem::transmute(caller) }; + /// Create a new subtree with this scope as the root of the subtree. + /// + /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route + /// the mutations to the correct window/portal/subtree. + /// + /// This method + /// + /// # Example + /// + /// ```rust + /// fn App(cx: Context, props: &()) -> Element { + /// todo!(); + /// rsx!(cx, div { "Subtree {id}"}) + /// }; + /// ``` + pub fn create_subtree(&self) -> Option { + if self.is_subtree_root.get() { + None + } else { + todo!() + // let cur = self.subtree().get(); + // self.shared.cur_subtree.set(cur + 1); + // Some(cur) + } } - /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from - /// causing UB in our tree. + /// Get the subtree ID that this scope belongs to. /// - /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components - /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove - /// any possible references to the data in the hook list. + /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route + /// the mutations to the correct window/portal/subtree. /// - /// References 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. + /// # Example /// - /// 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 we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Reference) - // right now, we don't drop - 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() - .expect("VComponents should be associated with a valid Scope"); + /// ```rust + /// fn App(cx: Context, props: &()) -> Element { + /// let id = cx.get_current_subtree(); + /// rsx!(cx, div { "Subtree {id}"}) + /// }; + /// ``` + pub fn get_current_subtree(&self) -> u32 { + self.subtree() + } - if let Some(scope) = pool.get_scope_mut(scope_id) { - scope.ensure_drop_safety(pool); + /// Store a value between renders + /// + /// This is *the* foundational hook for all other hooks. + /// + /// - Initializer: closure used to create the initial hook state + /// - Runner: closure used to output a value every time the hook is used + /// + /// To "cleanup" the hook, implement `Drop` on the stored hook value. Whenever the component is dropped, the hook + /// will be dropped as well. + /// + /// # Example + /// + /// ```ignore + /// // use_ref is the simplest way of storing a value between renders + /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell { + /// use_hook( + /// || Rc::new(RefCell::new(initial_value())), + /// |state| state, + /// ) + /// } + /// ``` + pub fn use_hook<'src, State: 'static, Output: 'src>( + &'src self, + initializer: impl FnOnce(usize) -> State, + runner: impl FnOnce(&'src mut State) -> Output, + ) -> Output { + if self.hooks.at_end() { + self.hooks.push_hook(initializer(self.hooks.len())); + } - let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); - drop_props(); - } - }); + const HOOK_ERR_MSG: &str = r###" +Unable to retrieve the hook that was initialized at this index. +Consult the `rules of hooks` to understand how to use hooks properly. - // Now that all the references are gone, we can safely drop our own references in our listeners. - self.listeners - .get_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|listener| drop(listener.callback.borrow_mut().take())); +You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. +Functions prefixed with "use" should never be called conditionally. +"###; + + runner(self.hooks.next::().expect(HOOK_ERR_MSG)) + } +} + +// Important internal methods +impl Scope { + /// The "work in progress frame" represents the frame that is currently being worked on. + pub(crate) fn wip_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + } + } + + pub(crate) fn fin_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 1 { + true => &self.frames[0], + false => &self.frames[1], + } + } + + pub unsafe fn reset_wip_frame(&self) { + // todo: unsafecell or something + let bump = self.wip_frame() as *const _ as *mut Bump; + let g = &mut *bump; + g.reset(); + } + + pub fn cycle_frame(&self) { + self.generation.set(self.generation.get() + 1); } /// A safe wrapper around calling listeners - pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { - let listners = self.listeners.borrow_mut(); + pub(crate) fn call_listener(&self, event: UserEvent, element: ElementId) { + let listners = &mut self.items.borrow_mut().listeners; - let raw_listener = listners.iter().find(|lis| { - let search = unsafe { &***lis }; + let listener = listners.iter().find(|lis| { + let search = lis; if search.event == event.name { let search_id = search.mounted_node.get(); search_id.map(|f| f == element).unwrap_or(false) @@ -276,8 +469,7 @@ impl ScopeInner { } }); - if let Some(raw_listener) = raw_listener { - let listener = unsafe { &**raw_listener }; + if let Some(listener) = listener { let mut cb = listener.callback.borrow_mut(); if let Some(cb) = cb.as_mut() { (cb)(event.event); @@ -287,89 +479,137 @@ impl ScopeInner { } } - /* - General strategy here is to load up the appropriate suspended task and then run it. - Suspended nodes cannot be called repeatedly. - */ + // 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(); + let mut nodes = &mut self.items.get_mut().suspended_nodes; 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 cx: SuspendedContext<'a> = SuspendedContext { - inner: Context { scope: self }, - }; - - let mut cb = sus.callback.borrow_mut().take().unwrap(); - - let new_node: Element<'a> = (cb)(cx); + let mut boxed = sus.callback.borrow_mut().take().unwrap(); + let new_node: Element = boxed(); } } // run the list of effects - pub(crate) fn run_effects(&mut self, pool: &ResourcePool) { - todo!() - // let mut effects = self.frames.effects.borrow_mut(); - // let mut effects = effects.drain(..).collect::>(); - - // for effect in effects { - // let effect = unsafe { &*effect }; - // let effect = effect.as_ref(); - - // let mut effect = effect.borrow_mut(); - // let mut effect = effect.as_mut(); - - // effect.run(pool); - // } - } - - /// 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); - - // 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()); - - log::debug!("Borrowed stuff is successfully cleared"); - - // Cast the caller ptr from static to one with our own reference - let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = unsafe { &*self.caller }; - - // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. - if let Some(builder) = render(self) { - let new_head = builder.into_vnode(NodeFactory { - bump: &self.frames.wip_frame().bump, - }); - log::debug!("Render is successful"); - - // 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 + pub(crate) fn run_effects(&mut self) { + for mut effect in self.items.get_mut().pending_effects.drain(..) { + effect(); } } + + pub fn root_node<'a>(&'a self) -> &'a VNode<'a> { + let node = *self.wip_frame().nodes.borrow().get(0).unwrap(); + unsafe { std::mem::transmute(&*node) } + } +} + +pub(crate) struct BumpFrame { + pub bump: Bump, + pub nodes: RefCell>>, +} +impl BumpFrame { + pub fn new(capacity: usize) -> Self { + let bump = Bump::with_capacity(capacity); + + let node = &*bump.alloc(VText { + text: "asd", + dom_id: Default::default(), + is_static: false, + }); + let node = bump.alloc(VNode::Text(unsafe { std::mem::transmute(node) })); + let nodes = RefCell::new(vec![node as *const _]); + Self { bump, nodes } + } + + pub fn allocated_bytes(&self) -> usize { + self.bump.allocated_bytes() + } + + pub fn assign_nodelink(&self, node: &NodeLink) { + let mut nodes = self.nodes.borrow_mut(); + + let len = nodes.len(); + nodes.push(node.node); + + node.link_idx.set(len); + } +} + +/// 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 { + arena: Bump, + vals: RefCell>, + idx: Cell, +} + +impl HookList { + pub fn new(capacity: usize) -> Self { + Self { + arena: Bump::with_capacity(capacity), + ..Default::default() + } + } + + 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 }; + raw_box.downcast_mut::() + }) + } + + /// 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 considered unsafe + /// + /// This should only be ran by Dioxus itself before "running scope". + /// Dioxus knows how to descend through the tree to prevent mutable aliasing. + pub(crate) unsafe fn reset(&self) { + self.idx.set(0); + } + + pub(crate) fn push_hook(&self, new: T) { + let val = self.arena.alloc(new); + self.vals.borrow_mut().push(val) + } + + pub(crate) fn len(&self) -> usize { + self.vals.borrow().len() + } + + pub(crate) fn cur_idx(&self) -> usize { + self.idx.get() + } + + pub(crate) fn at_end(&self) -> bool { + self.cur_idx() >= self.len() + } + + pub fn clear(&mut self) { + self.vals.borrow_mut().drain(..).for_each(|state| { + let as_mut = unsafe { &mut *state }; + let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; + drop(boxed); + }); + } + + /// Get the ammount of memory a hooklist uses + /// Used in heuristics + pub fn get_hook_arena_size(&self) -> usize { + self.arena.allocated_bytes() + } +} + +#[test] +fn sizeof() { + dbg!(std::mem::size_of::()); } diff --git a/packages/core/src/scopearena.rs b/packages/core/src/scopearena.rs new file mode 100644 index 000000000..c3c85d5d0 --- /dev/null +++ b/packages/core/src/scopearena.rs @@ -0,0 +1,361 @@ +use bumpalo::Bump; +use futures_channel::mpsc::UnboundedSender; +use fxhash::FxHashMap; +use slab::Slab; +use std::cell::{Cell, RefCell}; + +use crate::innerlude::*; + +pub type FcSlot = *const (); + +pub struct Heuristic { + hook_arena_size: usize, + node_arena_size: usize, +} + +// a slab-like arena with stable references even when new scopes are allocated +// uses a bump arena as a backing +// +// has an internal heuristics engine to pre-allocate arenas to the right size +pub(crate) struct ScopeArena { + bump: Bump, + scope_counter: Cell, + scopes: RefCell>, + pub heuristics: RefCell>, + free_scopes: RefCell>, + nodes: RefCell>>, + pub(crate) sender: UnboundedSender, +} + +impl ScopeArena { + pub fn new(sender: UnboundedSender) -> Self { + Self { + scope_counter: Cell::new(0), + bump: Bump::new(), + scopes: RefCell::new(FxHashMap::default()), + heuristics: RefCell::new(FxHashMap::default()), + free_scopes: RefCell::new(Vec::new()), + nodes: RefCell::new(Slab::new()), + sender, + } + } + + pub fn get_scope(&self, id: &ScopeId) -> Option<&Scope> { + unsafe { self.scopes.borrow().get(id).map(|f| &**f) } + } + + // this is unsafe + pub unsafe fn get_scope_raw(&self, id: &ScopeId) -> Option<*mut Scope> { + self.scopes.borrow().get(id).map(|f| *f) + } + // this is unsafe + + pub unsafe fn get_scope_mut(&self, id: &ScopeId) -> Option<&mut Scope> { + self.scopes.borrow().get(id).map(|s| &mut **s) + } + + pub fn new_with_key( + &self, + fc_ptr: *const (), + caller: *const dyn Fn(&Scope) -> Element, + parent_scope: Option<*mut Scope>, + height: u32, + subtree: u32, + ) -> ScopeId { + let new_scope_id = ScopeId(self.scope_counter.get()); + self.scope_counter.set(self.scope_counter.get() + 1); + + // + // + if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { + let scope = unsafe { &mut *old_scope }; + log::debug!( + "reusing scope {:?} as {:?}", + scope.our_arena_idx, + new_scope_id + ); + + scope.caller = caller; + scope.parent_scope = parent_scope; + scope.height = height; + scope.subtree = Cell::new(subtree); + scope.our_arena_idx = new_scope_id; + + scope.frames[0].nodes.get_mut().push({ + let vnode = scope.frames[0] + .bump + .alloc(VNode::Text(scope.frames[0].bump.alloc(VText { + dom_id: Default::default(), + is_static: false, + text: "", + }))); + unsafe { std::mem::transmute(vnode as *mut VNode) } + }); + + scope.frames[1].nodes.get_mut().push({ + let vnode = scope.frames[1] + .bump + .alloc(VNode::Text(scope.frames[1].bump.alloc(VText { + dom_id: Default::default(), + is_static: false, + text: "", + }))); + unsafe { std::mem::transmute(vnode as *mut VNode) } + }); + + let r = self.scopes.borrow_mut().insert(new_scope_id, scope); + + assert!(r.is_none()); + + new_scope_id + } else { + let (node_capacity, hook_capacity) = { + let heuristics = self.heuristics.borrow(); + if let Some(heuristic) = heuristics.get(&fc_ptr) { + (heuristic.node_arena_size, heuristic.hook_arena_size) + } else { + (0, 0) + } + }; + + let mut frames = [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)]; + + frames[0].nodes.get_mut().push({ + let vnode = frames[0] + .bump + .alloc(VNode::Text(frames[0].bump.alloc(VText { + dom_id: Default::default(), + is_static: false, + text: "", + }))); + unsafe { std::mem::transmute(vnode as *mut VNode) } + }); + + frames[1].nodes.get_mut().push({ + let vnode = frames[1] + .bump + .alloc(VNode::Text(frames[1].bump.alloc(VText { + dom_id: Default::default(), + is_static: false, + text: "", + }))); + unsafe { std::mem::transmute(vnode as *mut VNode) } + }); + + let scope = self.bump.alloc(Scope { + sender: self.sender.clone(), + our_arena_idx: new_scope_id, + parent_scope, + height, + frames, + subtree: Cell::new(subtree), + is_subtree_root: Cell::new(false), + + caller, + generation: 0.into(), + + hooks: HookList::new(hook_capacity), + shared_contexts: Default::default(), + + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + suspended_nodes: Default::default(), + tasks: Default::default(), + pending_effects: Default::default(), + }), + }); + + dbg!(self.scopes.borrow()); + + let r = self.scopes.borrow_mut().insert(new_scope_id, scope); + + assert!(r.is_none()); + // .expect(&format!("scope shouldnt exist, {:?}", new_scope_id)); + + new_scope_id + } + } + + pub fn try_remove(&self, id: &ScopeId) -> Option<()> { + self.ensure_drop_safety(id); + + log::debug!("removing scope {:?}", id); + println!("removing scope {:?}", id); + + let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; + + // we're just reusing scopes so we need to clear it out + scope.hooks.clear(); + scope.shared_contexts.get_mut().clear(); + scope.parent_scope = None; + scope.generation.set(0); + scope.is_subtree_root.set(false); + scope.subtree.set(0); + + scope.frames[0].nodes.get_mut().clear(); + scope.frames[1].nodes.get_mut().clear(); + + scope.frames[0].bump.reset(); + scope.frames[1].bump.reset(); + + let SelfReferentialItems { + borrowed_props, + listeners, + pending_effects, + suspended_nodes, + tasks, + } = scope.items.get_mut(); + + borrowed_props.clear(); + listeners.clear(); + pending_effects.clear(); + suspended_nodes.clear(); + tasks.clear(); + + self.free_scopes.borrow_mut().push(scope); + + Some(()) + } + + pub fn reserve_node(&self, node: &VNode) -> ElementId { + let mut els = self.nodes.borrow_mut(); + let entry = els.vacant_entry(); + let key = entry.key(); + let id = ElementId(key); + let node: *const VNode = node as *const _; + let node = unsafe { std::mem::transmute::<*const VNode, *const VNode>(node) }; + entry.insert(node); + id + } + + pub fn collect_garbage(&self, id: ElementId) { + self.nodes.borrow_mut().remove(id.0); + } + + // These methods would normally exist on `scope` but they need access to *all* of the scopes + + /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from + /// causing UB in our tree. + /// + /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components + /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove + /// any possible references to the data in the hook list. + /// + /// References 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(&self, scope_id: &ScopeId) { + let scope = self.get_scope(scope_id).unwrap(); + + let mut items = scope.items.borrow_mut(); + + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Reference) + // recursively call ensure_drop_safety on all children + items.borrowed_props.drain(..).for_each(|comp| { + let scope_id = comp + .associated_scope + .get() + .expect("VComponents should be associated with a valid Scope"); + + self.ensure_drop_safety(&scope_id); + + 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. + items + .listeners + .drain(..) + .for_each(|listener| drop(listener.callback.borrow_mut().take())); + } + + pub(crate) fn run_scope(&self, id: &ScopeId) -> bool { + let scope = unsafe { &mut *self.get_scope_mut(id).expect("could not find scope") }; + + log::debug!("found scope, about to run: {:?}", id); + + // 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(id); + + // 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 { scope.hooks.reset() }; + + // Safety: + // - We've dropped all references to the wip bump frame with "ensure_drop_safety" + unsafe { scope.reset_wip_frame() }; + + { + let mut items = scope.items.borrow_mut(); + + // just forget about our suspended nodes while we're at it + items.suspended_nodes.clear(); + items.tasks.clear(); + items.pending_effects.clear(); + + // guarantee that we haven't screwed up - there should be no latent references anywhere + debug_assert!(items.listeners.is_empty()); + debug_assert!(items.borrowed_props.is_empty()); + debug_assert!(items.suspended_nodes.is_empty()); + debug_assert!(items.tasks.is_empty()); + debug_assert!(items.pending_effects.is_empty()); + + // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. + scope.wip_frame().nodes.borrow_mut().clear(); + } + + let render: &dyn Fn(&Scope) -> Element = unsafe { &*scope.caller }; + + if let Some(link) = render(scope) { + // right now, it's a panic to render a nodelink from another scope + // todo: enable this. it should (reasonably) work even if it doesnt make much sense + assert_eq!(link.scope_id.get(), Some(*id)); + + // nodelinks are not assigned when called and must be done so through the create/diff phase + // however, we need to link this one up since it will never be used in diffing + scope.wip_frame().assign_nodelink(&link); + debug_assert_eq!(scope.wip_frame().nodes.borrow().len(), 1); + + if !scope.items.borrow().tasks.is_empty() { + // self. + } + + // make the "wip frame" contents the "finished frame" + // any future dipping into completed nodes after "render" will go through "fin head" + scope.cycle_frame(); + true + } else { + false + } + } + + // The head of the bumpframe is the first linked NodeLink + pub fn wip_head(&self, id: &ScopeId) -> &VNode { + let scope = self.get_scope(id).unwrap(); + let frame = scope.wip_frame(); + let nodes = frame.nodes.borrow(); + let node: &VNode = unsafe { &**nodes.get(0).unwrap() }; + unsafe { std::mem::transmute::<&VNode, &VNode>(node) } + } + + // The head of the bumpframe is the first linked NodeLink + pub fn fin_head(&self, id: &ScopeId) -> &VNode { + let scope = self.get_scope(id).unwrap(); + let frame = scope.fin_frame(); + let nodes = frame.nodes.borrow(); + let node: &VNode = unsafe { &**nodes.get(0).unwrap() }; + unsafe { std::mem::transmute::<&VNode, &VNode>(node) } + } + + pub fn root_node(&self, id: &ScopeId) -> &VNode { + self.fin_head(id) + } +} diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs deleted file mode 100644 index 95540d153..000000000 --- a/packages/core/src/tasks.rs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index aab7f3373..000000000 --- a/packages/core/src/test_dom.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! A DOM for testing - both internal and external code. -use bumpalo::Bump; - -use crate::innerlude::*; -use crate::nodes::IntoVNode; - -pub struct TestDom { - bump: Bump, - scheduler: Scheduler, -} - -impl TestDom { - pub fn new() -> TestDom { - let bump = Bump::new(); - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let scheduler = Scheduler::new(sender, receiver); - TestDom { bump, scheduler } - } - - pub fn new_factory(&self) -> NodeFactory { - NodeFactory::new(&self.bump) - } - - pub fn render_direct<'a>(&'a self, lazy_nodes: Option>) -> VNode<'a> { - lazy_nodes.into_vnode(NodeFactory::new(&self.bump)) - } - - pub fn render<'a>(&'a self, lazy_nodes: Option>) -> &'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::Diff { new, old }); - machine.mutations - } - - pub fn create<'a>(&'a self, left: Option>) -> Mutations<'a> { - let old = self.bump.alloc(self.render_direct(left)); - - let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); - - machine.stack.create_node(old, MountType::Append); - - machine.work(&mut || false); - - machine.mutations - } - - pub fn lazy_diff<'a>( - &'a self, - left: Option>, - right: Option>, - ) -> (Mutations<'a>, Mutations<'a>) { - 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(|| false); - let create_edits = machine.mutations; - - let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); - - machine.stack.push(DiffInstruction::Diff { old, new }); - - machine.work(&mut || false); - - let edits = machine.mutations; - - (create_edits, edits) - } -} - -impl Default for TestDom { - fn default() -> Self { - Self::new() - } -} diff --git a/packages/core/src/threadsafe.rs b/packages/core/src/threadsafe.rs deleted file mode 100644 index 813c1cee4..000000000 --- a/packages/core/src/threadsafe.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! A threadsafe wrapper for the VirtualDom -//! -//! This is an experimental module, and must be explicitly opted-into. -//! -//! It's not guaranteed that this module produces safe results, so use at your own peril. -//! -//! The only real "right" answer to a Send VirtualDom is by ensuring all hook data is Send -//! -//! -use std::sync::{Arc, Mutex, MutexGuard}; - -use crate::VirtualDom; - -/// A threadsafe wrapper for the Dioxus VirtualDom. -/// -/// The Dioxus VirtualDom is not normally `Send` because user code can contain non-`Send` types. However, it is important -/// to have a VirtualDom that is `Send` when used in server-side code since very few web frameworks support non-send -/// handlers. -/// -/// To address this, we have the `ThreadsafeVirtualDom` type which is a threadsafe wrapper for the VirtualDom. To access -/// the VirtualDom, it must be first unlocked using the `lock` method. This locks the VirtualDom through a mutex and -/// prevents any user code from leaking out. It is not possible to acquire any non-`Send` types from inside the VirtualDom. -/// -/// The only way data may be accessed through the VirtualDom is from the "root props" method or by accessing a `Scope` -/// directly. Even then, it's not possible to access any hook data. This means that non-Send types are only "in play" -/// while the VirtualDom is locked with a non-Send marker. -/// -/// Calling "wait for work" on the ThreadsafeVirtualDom does indeed work, because this method only accesses `Send` types. -/// Otherwise, the VirtualDom must be unlocked on the current thread to modify any data. -/// -/// Dioxus does have the concept of local tasks and non-local tasks. -/// -/// For the ThreadsafeVirtualDom, non-Send tasks are not ran - and will error out during a Debug build if one is submitted. -/// -/// -/// -/// When Tasks are submitted to a thread-local executor, -/// -pub struct ThreadsafeVirtualDom { - inner: Arc>, -} - -impl ThreadsafeVirtualDom { - pub fn new(inner: VirtualDom) -> Self { - let inner = Arc::new(Mutex::new(inner)); - Self { inner } - } - - pub fn lock(&self) -> Option { - let locked = self.inner.lock().unwrap(); - Some(VirtualDomGuard { guard: locked }) - } -} - -unsafe impl Send for ThreadsafeVirtualDom {} - -pub struct VirtualDomGuard<'a> { - guard: MutexGuard<'a, VirtualDom>, -} - -impl<'a> std::ops::Deref for VirtualDomGuard<'a> { - type Target = MutexGuard<'a, VirtualDom>; - - fn deref(&self) -> &Self::Target { - &self.guard - } -} - -impl<'a> std::ops::DerefMut for VirtualDomGuard<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.guard - } -} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs deleted file mode 100644 index 1ddd3a51c..000000000 --- a/packages/core/src/util.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::cell::Cell; -use std::fmt::Display; - -// create a cell with a "none" value -#[inline] -pub fn empty_cell() -> Cell> { - Cell::new(None) -} - -pub fn type_name_of(_: T) -> &'static str { - std::any::type_name::() -} - -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; - -// use crate::task::{Context, Poll}; - -/// Cooperatively gives up a timeslice to the task scheduler. -/// -/// Calling this function will move the currently executing future to the back -/// of the execution queue, making room for other futures to execute. This is -/// especially useful after running CPU-intensive operations inside a future. -/// -/// See also [`task::spawn_blocking`]. -/// -/// [`task::spawn_blocking`]: fn.spawn_blocking.html -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # async_std::task::block_on(async { -/// # -/// use async_std::task; -/// -/// task::yield_now().await; -/// # -/// # }) -/// ``` -#[inline] -pub async fn yield_now() { - YieldNow(false).await -} - -struct YieldNow(bool); - -impl Future for YieldNow { - type Output = (); - - // The futures executor is implemented as a FIFO queue, so all this future - // does is re-schedule the future back to the end of the queue, giving room - // for other futures to progress. - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if !self.0 { - self.0 = true; - cx.waker().wake_by_ref(); - Poll::Pending - } else { - Poll::Ready(()) - } - } -} - -/// 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. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(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 7f2765c6f..c90fa0119 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,72 +1,127 @@ //! # 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 -//! navigate the inner workings of the Dom. We try to keep these main mechanics in this file to limit -//! the possible exposed API surface (keep fields private). This particular implementation of VDOM -//! is extremely efficient, but relies on some unsafety under the hood to do things like manage -//! micro-heaps for components. We are currently working on refactoring the safety out into safe(r) -//! abstractions, but current tests (MIRI and otherwise) show no issues with the current implementation. -//! -//! Included is: -//! - The [`VirtualDom`] itself -//! - The [`Scope`] object for managing component lifecycle -//! - The [`ActiveFrame`] object for managing the Scope`s microheap -//! - The [`Context`] object for exposing VirtualDOM API to components -//! - The [`NodeFactory`] object for lazily exposing the `Context` API to the nodebuilder API -//! -//! This module includes just the barebones for a complete VirtualDOM API. -//! Additional functionality is defined in the respective files. - -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use crate::innerlude::*; -use std::{any::Any, rc::Rc}; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures_util::{Future, StreamExt}; +use fxhash::FxHashSet; +use indexmap::IndexSet; +use std::pin::Pin; +use std::task::Poll; +use std::{any::Any, collections::VecDeque}; -/// An integrated virtual node system that progresses events and diffs UI trees. +/// A virtual node system that progresses user 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. +/// ## Guide +/// +/// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`]. /// -/// Example /// ```rust -/// static App: FC<()> = |(cx, props)|{ +/// #[derive(Props, PartialEq)] +/// struct AppProps { +/// title: String +/// } +/// +/// fn App(cx: Context, props: &AppProps) -> Element { +/// cx.render(rsx!( +/// div {"hello, {props.title}"} +/// )) +/// } +/// ``` +/// +/// Components may be composed to make complex apps. +/// +/// ```rust +/// fn App(cx: Context, props: &AppProps) -> Element { +/// cx.render(rsx!( +/// NavBar { routes: ROUTES } +/// Title { "{props.title}" } +/// Footer {} +/// )) +/// } +/// ``` +/// +/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to +/// draw the UI. +/// +/// ```rust +/// let mut vdom = VirtualDom::new(App); +/// let edits = vdom.rebuild(); +/// ``` +/// +/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. +/// +/// ```rust +/// let channel = vdom.get_scheduler_channel(); +/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { +/// // ... +/// })) +/// ``` +/// +/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. +/// +/// ```rust +/// vdom.wait_for_work().await; +/// ``` +/// +/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and +/// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be +/// handled by the renderer. +/// +/// ```rust +/// let mutations = vdom.work_with_deadline(|| false); +/// for edit in mutations { +/// apply(edit); +/// } +/// ``` +/// +/// ## Building an event loop around Dioxus: +/// +/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. +/// +/// ```rust +/// fn App(cx: Context, props: &()) -> Element { /// cx.render(rsx!{ -/// div { -/// "Hello World" -/// } +/// div { "Hello World" } /// }) /// } /// /// async fn main() { /// let mut dom = VirtualDom::new(App); +/// /// let mut inital_edits = dom.rebuild(); -/// initialize_screen(inital_edits); +/// apply_edits(inital_edits); /// /// loop { -/// let next_frame = TimeoutFuture::new(Duration::from_millis(16)); -/// let edits = dom.run_with_deadline(next_frame).await; +/// dom.wait_for_work().await; +/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16)); +/// let deadline = || (&mut frame_timeout).now_or_never(); +/// let edits = dom.run_with_deadline(deadline).await; /// apply_edits(edits); -/// render_frame(); /// } /// } /// ``` pub struct VirtualDom { - scheduler: Scheduler, - base_scope: ScopeId, - root_fc: Box, + _root_caller: *mut dyn Fn(&Scope) -> Element, - root_props: Rc, + scopes: Box, - // we need to keep the allocation around, but we don't necessarily use it - _root_caller: Rc, + receiver: UnboundedReceiver, + + sender: UnboundedSender, + + pending_futures: FxHashSet, + + pending_messages: VecDeque, + + dirty_scopes: IndexSet, } +// Methods to create the VirtualDom impl VirtualDom { /// Create a new VirtualDOM with a component that does not have special props. /// @@ -80,7 +135,7 @@ impl VirtualDom { /// /// # Example /// ``` - /// fn Example(cx: Context<()>) -> DomTree { + /// fn Example(cx: Context, props: &()) -> Element { /// cx.render(rsx!( div { "hello world" } )) /// } /// @@ -109,7 +164,7 @@ impl VirtualDom { /// name: &'static str /// } /// - /// fn Example(cx: Context) -> DomTree { + /// fn Example(cx: Context, props: &SomeProps) -> Element { /// cx.render(rsx!{ div{ "hello {cx.name}" } }) /// } /// @@ -122,12 +177,12 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props(root: FC

, root_props: P) -> Self { + pub fn new_with_props(root: FC

, root_props: P) -> Self { let (sender, receiver) = futures_channel::mpsc::unbounded::(); Self::new_with_props_and_scheduler(root, root_props, sender, receiver) } - /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler. + /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler /// /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the /// VirtualDom to be created just to retrieve its channel receiver. @@ -137,38 +192,26 @@ impl VirtualDom { sender: UnboundedSender, receiver: UnboundedReceiver, ) -> Self { - let root_fc = Box::new(root); + let scopes = ScopeArena::new(sender.clone()); - let root_props: Rc = Rc::new(root_props); + let caller = Box::new(move |scp: &Scope| -> Element { root(scp, &root_props) }); + let caller_ref: *mut dyn Fn(&Scope) -> Element = Box::into_raw(caller); + let base_scope = scopes.new_with_key(root as _, caller_ref, None, 0, 0); - let props = root_props.clone(); - - let root_caller: Rc Element> = Rc::new(move |scope: &ScopeInner| { - let props = props.downcast_ref::

().unwrap(); - let node = root((Context { scope }, props)); - // cast into the right lifetime - unsafe { std::mem::transmute(node) } - }); - - let scheduler = Scheduler::new(sender, receiver); - - let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { - ScopeInner::new( - root_caller.as_ref(), - myidx, - None, - 0, - 0, - scheduler.pool.channel.clone(), - ) - }); + let pending_messages = VecDeque::new(); + let mut dirty_scopes = IndexSet::new(); + dirty_scopes.insert(base_scope); Self { - _root_caller: Rc::new(root_caller), - root_fc, + scopes: Box::new(scopes), base_scope, - scheduler, - root_props, + receiver, + // todo: clean this up manually? + _root_caller: caller_ref, + pending_messages, + pending_futures: Default::default(), + dirty_scopes, + sender, } } @@ -176,134 +219,117 @@ impl VirtualDom { /// /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus /// directly. - pub fn base_scope(&self) -> &ScopeInner { - self.scheduler.pool.get_scope(self.base_scope).unwrap() + /// + /// # Example + pub fn base_scope(&self) -> &Scope { + self.get_scope(&self.base_scope).unwrap() } /// Get the [`Scope`] for a component given its [`ScopeId`] - pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeInner> { - self.scheduler.pool.get_scope(id) - } - - /// Update the root props of this VirtualDOM. - /// - /// This method returns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately, - /// so calling this method will block the main thread until computation is done. - /// - /// ## Example - /// - /// ```rust - /// #[derive(Props, PartialEq)] - /// struct AppProps { - /// route: &'static str - /// } - /// 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

(&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: Rc = Rc::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 root_caller: Box Element> = - Box::new(move |scope: &ScopeInner| unsafe { - let props: &'_ P = &*(props_ptr as *const P); - std::mem::transmute(root((Context { scope }, props))) - }); - - root_scope.update_scope_dependencies(&root_caller); - - drop(root_props); - - Some(self.rebuild()) - } else { - None - } - } - - /// 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. - /// - /// 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. - /// - /// 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) + /// + /// + pub fn get_scope<'a>(&'a self, id: &ScopeId) -> Option<&'a Scope> { + self.scopes.get_scope(id) } - /// 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 propagated as an event. - /// - /// 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. - /// + /// Get an [`UnboundedSender`] handle to the channel used by the scheduler. /// /// # 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) + pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender { + self.sender.clone() } - /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. + /// Check if the [`VirtualDom`] has any pending updates or work to be done. /// - /// 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(&mut self) -> Option> { - if self.scheduler.has_any_work() { - Some(self.scheduler.work_sync()) + /// # Example + /// + /// ```rust + /// + /// + /// ``` + pub fn has_any_work(&self) -> bool { + !(self.dirty_scopes.is_empty() && self.pending_messages.is_empty()) + } + + /// 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) { + // todo: poll the events once even if there is work to do to prevent starvation + + // if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed + if self.pending_futures.is_empty() { + self.pending_messages + .push_front(self.receiver.next().await.unwrap()); } else { - None + struct PollTasks<'a> { + pending_futures: &'a FxHashSet, + scopes: &'a ScopeArena, + } + + impl<'a> Future for PollTasks<'a> { + type Output = (); + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll { + let mut all_pending = true; + + // Poll every scope manually + for fut in self.pending_futures.iter() { + let scope = self + .scopes + .get_scope(fut) + .expect("Scope should never be moved"); + + let mut items = scope.items.borrow_mut(); + for task in items.tasks.iter_mut() { + let task = task.as_mut(); + + // todo: does this make sense? + // I don't usually write futures by hand + // I think the futures neeed to be pinned using bumpbox or something + // right now, they're bump allocated so this shouldn't matter anyway - they're not going to move + let unpinned = unsafe { Pin::new_unchecked(task) }; + + if unpinned.poll(cx).is_ready() { + all_pending = false + } + } + } + + // Resolve the future if any singular task is ready + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + } + } + + // Poll both the futures and the scheduler message queue simulataneously + use futures_util::future::{select, Either}; + + let scheduler_fut = self.receiver.next(); + let tasks_fut = PollTasks { + pending_futures: &self.pending_futures, + scopes: &self.scopes, + }; + + match select(tasks_fut, scheduler_fut).await { + // Futures don't generate work + Either::Left((_, _)) => {} + + // Save these messages in FIFO to be processed later + Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), + } } } @@ -334,11 +360,18 @@ impl VirtualDom { /// # Example /// /// ```no_run - /// static App: FC<()> = |(cx, props)|rsx!(cx, div {"hello"} ); + /// fn App(cx: Context, props: &()) -> Element { + /// cx.render(rsx!( div {"hello"} )) + /// } + /// /// let mut dom = VirtualDom::new(App); + /// /// loop { - /// let deadline = TimeoutFuture::from_ms(16); + /// let mut timeout = TimeoutFuture::from_ms(16); + /// let deadline = move || (&mut timeout).now_or_never(); + /// /// let mutations = dom.run_with_deadline(deadline).await; + /// /// apply_mutations(mutations); /// } /// ``` @@ -350,48 +383,312 @@ impl VirtualDom { /// applied the edits. /// /// Mutations are the only link between the RealDOM and the VirtualDOM. - pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec> { - self.scheduler.work_with_deadline(deadline) - } + /// + pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { + let mut committed_mutations = vec![]; - pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.scheduler.pool.channel.sender.clone() - } - - /// 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) { - // todo: poll the events once even if there is work to do to prevent starvation - if self.scheduler.has_any_work() { - return; - } - - use futures_util::StreamExt; - - // Wait for any new events if we have nothing to do - - let tasks_fut = self.scheduler.async_tasks.next(); - let scheduler_fut = self.scheduler.receiver.next(); - - use futures_util::future::{select, Either}; - match select(tasks_fut, scheduler_fut).await { - // poll the internal futures - Either::Left((_id, _)) => { - // + while self.has_any_work() { + while let Ok(Some(msg)) = self.receiver.try_next() { + self.pending_messages.push_front(msg); } - // wait for an external event - Either::Right((msg, _)) => match msg.unwrap() { - SchedulerMsg::Task(t) => { - self.scheduler.handle_task(t); + while let Some(msg) = self.pending_messages.pop_back() { + match msg { + SchedulerMsg::Immediate(id) => { + self.dirty_scopes.insert(id); + } + SchedulerMsg::UiEvent(event) => { + if let Some(element) = event.mounted_dom_id { + log::info!("Calling listener {:?}, {:?}", event.scope_id, element); + + if let Some(scope) = self.scopes.get_scope(&event.scope_id) { + // TODO: bubble properly here + scope.call_listener(event, element); + + while let Ok(Some(dirty_scope)) = self.receiver.try_next() { + self.pending_messages.push_front(dirty_scope); + } + } + } else { + log::debug!("User event without a targetted ElementId. Not currently supported.\nUnsure how to proceed. {:?}", event); + } + } } - SchedulerMsg::Immediate(im) => { - self.scheduler.dirty_scopes.insert(im); + } + + let scopes = &self.scopes; + let mut diff_state = DiffState::new(scopes); + + let mut ran_scopes = FxHashSet::default(); + + // todo: the 2021 version of rust will let us not have to force the borrow + // let scopes = &self.scopes; + // Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height + self.dirty_scopes + .retain(|id| scopes.get_scope(id).is_some()); + + self.dirty_scopes.sort_by(|a, b| { + let h1 = scopes.get_scope(a).unwrap().height; + let h2 = scopes.get_scope(b).unwrap().height; + h1.cmp(&h2).reverse() + }); + + if let Some(scopeid) = self.dirty_scopes.pop() { + log::info!("handling dirty scope {:?}", scopeid); + + if !ran_scopes.contains(&scopeid) { + ran_scopes.insert(scopeid); + + log::debug!("about to run scope {:?}", scopeid); + + if self.scopes.run_scope(&scopeid) { + let (old, new) = ( + self.scopes.wip_head(&scopeid), + self.scopes.fin_head(&scopeid), + ); + diff_state.stack.scope_stack.push(scopeid); + diff_state.stack.push(DiffInstruction::Diff { new, old }); + } } - SchedulerMsg::UiEvent(evt) => { - self.scheduler.ui_events.push_back(evt); + } + + let work_completed = diff_state.work(&mut deadline); + + if work_completed { + let DiffState { + mutations, + seen_scopes, + stack, + .. + } = diff_state; + + for scope in seen_scopes { + self.dirty_scopes.remove(&scope); } - }, + + // // I think the stack should be empty at the end of diffing? + // debug_assert_eq!(stack.scope_stack.len(), 1); + + committed_mutations.push(mutations); + } else { + // leave the work in an incomplete state + log::debug!("don't have a mechanism to pause work (yet)"); + return committed_mutations; + } } + + committed_mutations + } + + /// 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. + /// + /// 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. + /// + /// 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 { + let mut diff_state = DiffState::new(&self.scopes); + + let scope_id = self.base_scope; + if self.scopes.run_scope(&scope_id) { + diff_state + .stack + .create_node(self.scopes.fin_head(&scope_id), MountType::Append); + + diff_state.stack.scope_stack.push(scope_id); + + diff_state.work(|| false); + } + + diff_state.mutations + } + + /// 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 propagated as an event. + /// + /// 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 hard_diff<'a>(&'a mut self, scope_id: &ScopeId) -> Option> { + let mut diff_machine = DiffState::new(&self.scopes); + if self.scopes.run_scope(scope_id) { + diff_machine.force_diff = true; + diff_machine.diff_scope(scope_id); + } + Some(diff_machine.mutations) + } + + /// Renders an `rsx` call into the Base Scope's allocator. + /// + /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + pub fn render_vnodes<'a>(&'a self, lazy_nodes: Option>) -> &'a VNode<'a> { + let scope = self.scopes.get_scope(&self.base_scope).unwrap(); + let frame = scope.wip_frame(); + let factory = NodeFactory { bump: &frame.bump }; + let node = lazy_nodes.unwrap().call(factory); + frame.bump.alloc(node) + } + + /// Renders an `rsx` call into the Base Scope's allocator. + /// + /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { + let mut machine = DiffState::new(&self.scopes); + machine.stack.push(DiffInstruction::Diff { new, old }); + machine.stack.scope_stack.push(self.base_scope); + machine.work(|| false); + machine.mutations + } + + /// Renders an `rsx` call into the Base Scope's allocator. + /// + /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + pub fn create_vnodes<'a>(&'a self, left: Option>) -> Mutations<'a> { + let nodes = self.render_vnodes(left); + let mut machine = DiffState::new(&self.scopes); + machine.stack.create_node(nodes, MountType::Append); + machine.work(|| false); + machine.mutations + } + + /// Renders an `rsx` call into the Base Scope's allocator. + /// + /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + pub fn diff_lazynodes<'a>( + &'a self, + left: Option>, + right: Option>, + ) -> (Mutations<'a>, Mutations<'a>) { + let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); + + let mut create = DiffState::new(&self.scopes); + create.stack.scope_stack.push(self.base_scope); + create.stack.create_node(old, MountType::Append); + create.work(|| false); + + let mut edit = DiffState::new(&self.scopes); + create.stack.scope_stack.push(self.base_scope); + edit.stack.push(DiffInstruction::Diff { old, new }); + edit.work(&mut || false); + + (create.mutations, edit.mutations) } } + +pub enum SchedulerMsg { + // events from the host + UiEvent(UserEvent), + + // setstate + Immediate(ScopeId), +} + +#[derive(Debug)] +pub struct UserEvent { + /// The originator of the event trigger + pub scope_id: ScopeId, + + pub priority: EventPriority, + + /// The optional real node associated with the trigger + pub mounted_dom_id: Option, + + /// The event type IE "onclick" or "onmouseover" + /// + /// The name that the renderer will use to mount the listener. + pub name: &'static str, + + /// Event Data + pub event: Box, +} + +/// 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 precedence 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 preempted 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, +} diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index c46a8fe3c..692a8f8db 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -10,8 +10,8 @@ fn test_borrowed_state() { let _ = VirtualDom::new(Parent); } -fn Parent((cx, _): Scope<()>) -> Element { - let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {}); +fn Parent(cx: Context, props: &()) -> Element { + let value = cx.use_hook(|_| String::new(), |f| &*f); cx.render(rsx! { div { @@ -28,7 +28,7 @@ struct ChildProps<'a> { name: &'a str, } -fn Child<'a>((cx, props): Scope<'a, ChildProps>) -> Element<'a> { +fn Child(cx: Context, props: &ChildProps) -> Element { cx.render(rsx! { div { h1 { "it's nested" } @@ -42,7 +42,7 @@ struct Grandchild<'a> { name: &'a str, } -fn Child2<'a>((cx, props): Scope<'a, Grandchild>) -> Element<'a> { +fn Child2(cx: Context, props: &Grandchild) -> Element { cx.render(rsx! { div { "Hello {props.name}!" } }) diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 712bf3713..fdb9786a6 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -21,7 +21,7 @@ fn new_dom(app: FC

, props: P) -> VirtualDom { #[test] fn test_original_diff() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { @@ -57,17 +57,17 @@ fn test_original_diff() { #[test] fn create() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { "Hello, world!" div { div { - // Fragment { - // "hello" - // "world" - // } + Fragment { + "hello" + "world" + } } } } @@ -120,7 +120,7 @@ fn create() { #[test] fn create_list() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { {(0..3).map(|f| rsx!{ div { "hello" @@ -169,7 +169,7 @@ fn create_list() { #[test] fn create_simple() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div {} div {} @@ -207,7 +207,7 @@ fn create_simple() { } #[test] fn create_components() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx! { Child { "abc1" } Child { "abc2" } @@ -215,12 +215,12 @@ fn create_components() { }) }; - #[derive(Props)] - struct ChildProps<'a> { - children: ScopeChildren<'a>, + #[derive(Props, PartialEq)] + struct ChildProps { + children: Element, } - fn Child<'a>((cx, props): Scope<'a, ChildProps<'a>>) -> Element { + fn Child(cx: Context, props: &ChildProps) -> Element { cx.render(rsx! { h1 {} div { {&props.children} } @@ -273,7 +273,7 @@ fn create_components() { } #[test] fn anchors() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx! { {true.then(|| rsx!{ div { "hello" } })} {false.then(|| rsx!{ div { "goodbye" } })} @@ -299,20 +299,3 @@ fn anchors() { ] ); } - -#[test] -fn suspended() { - static App: FC<()> = |(cx, props)| { - let val = use_suspense(cx, || async {}, |cx, p| todo!()); - - cx.render(rsx! { {val} }) - }; - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 },] - ); -} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 65aece563..871732a0d 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -5,17 +5,17 @@ //! //! It does not validated that component lifecycles work properly. This is done in another test file. -use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom}; +use dioxus::{prelude::*, DomEdit, VSuspended}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; mod test_logging; -fn new_dom() -> TestDom { +fn new_dom() -> VirtualDom { const IS_LOGGING_ENABLED: bool = false; test_logging::set_up_logging(IS_LOGGING_ENABLED); - TestDom::new() + VirtualDom::new(|cx, props| todo!()) } use DomEdit::*; @@ -24,7 +24,7 @@ use DomEdit::*; #[test] fn html_and_rsx_generate_the_same_output() { let dom = new_dom(); - let (create, change) = dom.lazy_diff( + let (create, change) = dom.diff_lazynodes( rsx! ( div { "Hello world" } ), rsx! ( div { "Goodbye world" } ), ); @@ -58,7 +58,7 @@ fn html_and_rsx_generate_the_same_output() { fn fragments_create_properly() { let dom = new_dom(); - let create = dom.create(rsx! { + let create = dom.create_vnodes(rsx! { div { "Hello a" } div { "Hello b" } div { "Hello c" } @@ -107,7 +107,7 @@ fn empty_fragments_create_anchors() { 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); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, @@ -133,7 +133,7 @@ fn empty_fragments_create_many_anchors() { 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); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] @@ -179,7 +179,7 @@ fn empty_fragments_create_anchors_with_many_children() { }) }); - let (create, change) = dom.lazy_diff(left, right); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] @@ -231,7 +231,7 @@ fn many_items_become_fragment() { }); let right = rsx!({ (0..0).map(|_| rsx! { div {} }) }); - let (create, change) = dom.lazy_diff(left, right); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, [ @@ -257,12 +257,12 @@ fn many_items_become_fragment() { ] ); - // hmmmmmmmmm worried about reusing IDs that we shouldnt be + // note: the ID gets reused assert_eq!( change.edits, [ Remove { root: 2 }, - CreatePlaceholder { root: 4 }, + CreatePlaceholder { root: 3 }, ReplaceWith { root: 0, m: 1 }, ] ); @@ -284,7 +284,7 @@ fn two_equal_fragments_are_equal() { }) }); - let (_create, change) = dom.lazy_diff(left, right); + let (_create, change) = dom.diff_lazynodes(left, right); assert!(change.edits.is_empty()); } @@ -302,7 +302,7 @@ fn two_fragments_with_differrent_elements_are_differet() { p {} ); - let (_create, changes) = dom.lazy_diff(left, right); + let (_create, changes) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", &changes); assert_eq!( changes.edits, @@ -335,7 +335,7 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { p {} ); - let (create, change) = dom.lazy_diff(left, right); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, [ @@ -391,7 +391,7 @@ fn two_fragments_with_same_elements_are_differet() { p {} ); - let (create, change) = dom.lazy_diff(left, right); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( create.edits, [ @@ -433,7 +433,7 @@ fn keyed_diffing_order() { let dom = new_dom(); let left = rsx!( - // {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} + {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} p {"e"} ); let right = rsx!( @@ -441,7 +441,7 @@ fn keyed_diffing_order() { p {"e"} ); - let (create, change) = dom.lazy_diff(left, right); + let (create, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },] @@ -465,7 +465,7 @@ fn keyed_diffing_out_of_order() { }) }); - let (_, changes) = dom.lazy_diff(left, right); + let (_, changes) = dom.diff_lazynodes(left, right); log::debug!("{:?}", &changes); assert_eq!( changes.edits, @@ -490,7 +490,7 @@ fn keyed_diffing_out_of_order_adds() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [ @@ -517,7 +517,7 @@ fn keyed_diffing_out_of_order_adds_2() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [ @@ -545,7 +545,7 @@ fn keyed_diffing_out_of_order_adds_3() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [ @@ -573,7 +573,7 @@ fn keyed_diffing_out_of_order_adds_4() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [ @@ -601,7 +601,7 @@ fn keyed_diffing_out_of_order_adds_5() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [PushRoot { root: 4 }, InsertBefore { n: 1, root: 3 }] @@ -624,7 +624,7 @@ fn keyed_diffing_additions() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); assert_eq!( change.edits, [ @@ -657,7 +657,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { }) }); - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); log::debug!("{:?}", change); assert_eq!( change.edits, @@ -696,7 +696,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { }); // LIS: 4, 5, 6 - let (_, change) = dom.lazy_diff(left, right); + let (_, change) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", change); assert_eq!( change.edits, @@ -745,7 +745,7 @@ fn controlled_keyed_diffing_out_of_order() { }); // LIS: 5, 6 - let (_, changes) = dom.lazy_diff(left, right); + let (_, changes) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", &changes); assert_eq!( changes.edits, @@ -787,7 +787,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { }) }); - let (_, changes) = dom.lazy_diff(left, right); + let (_, changes) = dom.diff_lazynodes(left, right); log::debug!("{:#?}", &changes); assert_eq!( changes.edits, @@ -802,22 +802,3 @@ fn controlled_keyed_diffing_out_of_order_max_test() { ] ); } - -#[test] -fn suspense() { - let dom = new_dom(); - - todo!() - // let edits = dom.create(Some(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 { root: 0 }, AppendChildren { many: 1 }] - // ); -} diff --git a/packages/core/tests/display_vdom.rs b/packages/core/tests/display_vdom.rs deleted file mode 100644 index c7acc712c..000000000 --- a/packages/core/tests/display_vdom.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! test that we can display the virtualdom properly -//! -//! -//! - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; -mod test_logging; - -#[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/lifecycle.rs b/packages/core/tests/lifecycle.rs index 3b9c05cca..b5568d74f 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -3,7 +3,11 @@ //! Tests for the lifecycle of components. use dioxus::prelude::*; use dioxus_core as dioxus; +use dioxus_core::DomEdit::*; +use dioxus_core::ScopeId; + use dioxus_core_macro::*; +use dioxus_hooks::*; use dioxus_html as dioxus_elements; use std::sync::{Arc, Mutex}; @@ -20,7 +24,7 @@ fn manual_diffing() { value: Shared<&'static str>, } - static App: FC = |(cx, props)| { + static App: FC = |cx, props| { let val = props.value.lock().unwrap(); cx.render(rsx! { div { "{val}" } }) }; @@ -37,7 +41,275 @@ fn manual_diffing() { *value.lock().unwrap() = "goodbye"; - let edits = dom.diff(); + let edits = dom.rebuild(); log::debug!("edits: {:?}", edits); } + +#[test] +fn events_generate() { + static App: FC<()> = |cx, _| { + let mut count = use_state(cx, || 0); + + let inner = match *count { + 0 => { + rsx! { + div { + onclick: move |_| count += 1, + div { + "nested" + } + "Click me!" + } + } + } + _ => todo!(), + }; + + cx.render(inner) + }; + + let mut dom = VirtualDom::new(App); + let mut channel = dom.get_scheduler_channel(); + assert!(dom.has_any_work()); + + let edits = dom.work_with_deadline(|| false); + assert_eq!( + edits[0].edits, + [ + CreateElement { + tag: "div", + root: 0, + }, + NewEventListener { + event_name: "click", + scope: ScopeId(0), + root: 0, + }, + CreateElement { + tag: "div", + root: 1, + }, + CreateTextNode { + text: "nested", + root: 2, + }, + AppendChildren { many: 1 }, + CreateTextNode { + text: "Click me!", + root: 3, + }, + AppendChildren { many: 2 }, + ] + ) +} + +#[test] +fn components_generate() { + static App: FC<()> = |cx, _| { + let mut render_phase = use_state(cx, || 0); + render_phase += 1; + + cx.render(match *render_phase { + 0 => rsx!("Text0"), + 1 => rsx!(div {}), + 2 => rsx!("Text2"), + 3 => rsx!(Child {}), + 4 => rsx!({ None as Option<()> }), + 5 => rsx!("text 3"), + 6 => rsx!({ (0..2).map(|f| rsx!("text {f}")) }), + 7 => rsx!(Child {}), + _ => todo!(), + }) + }; + + static Child: FC<()> = |cx, _| { + println!("running child"); + cx.render(rsx! { + h1 {} + }) + }; + + let mut dom = VirtualDom::new(App); + let edits = dom.rebuild(); + assert_eq!( + edits.edits, + [ + CreateTextNode { + text: "Text0", + root: 0, + }, + AppendChildren { many: 1 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateElement { + tag: "div", + root: 1, + }, + ReplaceWith { root: 0, m: 1 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateTextNode { + text: "Text2", + root: 2, + }, + ReplaceWith { root: 1, m: 1 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateElement { tag: "h1", root: 3 }, + ReplaceWith { root: 2, m: 1 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [CreatePlaceholder { root: 4 }, ReplaceWith { root: 3, m: 1 },] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateTextNode { + text: "text 3", + root: 5, + }, + ReplaceWith { root: 4, m: 1 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateTextNode { + text: "text 0", + root: 6, + }, + CreateTextNode { + text: "text 1", + root: 7, + }, + ReplaceWith { root: 5, m: 2 }, + ] + ); + + let edits = dom.hard_diff(&ScopeId(0)).unwrap(); + assert_eq!( + edits.edits, + [ + CreateElement { tag: "h1", root: 8 }, + ReplaceWith { root: 6, m: 1 }, + Remove { root: 7 }, + ] + ); +} + +#[test] +fn component_swap() { + // simple_logger::init(); + static App: FC<()> = |cx, _| { + let mut render_phase = use_state(cx, || 0); + render_phase += 1; + + cx.render(match *render_phase { + 0 => rsx!( + div { + NavBar {} + Dashboard {} + } + ), + 1 => rsx!( + div { + NavBar {} + Results {} + } + ), + 2 => rsx!( + div { + NavBar {} + Dashboard {} + } + ), + 3 => rsx!( + div { + NavBar {} + Results {} + } + ), + 4 => rsx!( + div { + NavBar {} + Dashboard {} + } + ), + _ => rsx!("blah"), + }) + }; + + static NavBar: FC<()> = |cx, _| { + println!("running navbar"); + cx.render(rsx! { + h1 { + "NavBar" + {(0..3).map(|f| rsx!(NavLink {}))} + } + }) + }; + + static NavLink: FC<()> = |cx, _| { + println!("running navlink"); + cx.render(rsx! { + h1 { + "NavLink" + } + }) + }; + + static Dashboard: FC<()> = |cx, _| { + println!("running dashboard"); + cx.render(rsx! { + div { + "dashboard" + } + }) + }; + + static Results: FC<()> = |cx, _| { + println!("running results"); + cx.render(rsx! { + div { + "results" + } + }) + }; + + let mut dom = VirtualDom::new(App); + let edits = dom.rebuild(); + dbg!(&edits); + + let edits = dom.work_with_deadline(|| false); + dbg!(&edits); + let edits = dom.work_with_deadline(|| false); + dbg!(&edits); + let edits = dom.work_with_deadline(|| false); + dbg!(&edits); + let edits = dom.work_with_deadline(|| false); + dbg!(&edits); +} diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 7bb9e6501..bf57a5186 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -1,6 +1,6 @@ #![allow(unused, non_upper_case_globals)] -use dioxus::{prelude::*, DomEdit, Mutations, TestDom}; +use dioxus::{prelude::*, DomEdit, Mutations}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; @@ -13,12 +13,12 @@ mod test_logging; fn shared_state_test() { struct MySharedState(&'static str); - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.provide_state(MySharedState("world!")); cx.render(rsx!(Child {})) }; - static Child: FC<()> = |(cx, props)| { + static Child: FC<()> = |cx, props| { let shared = cx.consume_state::()?; cx.render(rsx!("Hello, {shared.0}")) }; diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/core/tests/task.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 79aaad112..9aa805918 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -17,7 +17,7 @@ use dioxus_html as dioxus_elements; #[test] fn app_runs() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { // cx.render(rsx!( div{"hello"} )) }; @@ -28,7 +28,7 @@ fn app_runs() { #[test] fn fragments_work() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( div{"hello"} div{"goodbye"} @@ -42,7 +42,7 @@ fn fragments_work() { #[test] fn lists_work() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {(0..6).map(|f| rsx!(span{ "{f}" }))} @@ -55,7 +55,7 @@ fn lists_work() { #[test] fn conditional_rendering() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {true.then(|| rsx!(span{ "a" }))} @@ -72,13 +72,13 @@ fn conditional_rendering() { #[test] fn child_components() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( {true.then(|| rsx!(Child { }))} {false.then(|| rsx!(Child { }))} )) }; - static Child: FC<()> = |(cx, props)| { + static Child: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} h1 {"goodbye"} @@ -91,13 +91,14 @@ fn child_components() { #[test] fn suspended_works() { - static App: FC<()> = |(cx, props)| { - let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!()); - // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"}); - cx.render(rsx!("hello" { title })) - }; + todo!() + // static App: FC<()> = |cx, props| { + // let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!()); + // // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"}); + // cx.render(rsx!("hello" { title })) + // }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); + // let mut vdom = VirtualDom::new(App); + // let edits = vdom.rebuild(); + // dbg!(edits); } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e6aef1378..142b76367 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -24,6 +24,7 @@ tokio = { version = "1.12.0", features = [ "rt", ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro" } +dioxus-html = { path = "../html", features = ["serialize"] } [features] default = ["tokio_runtime"] @@ -31,6 +32,5 @@ tokio_runtime = ["tokio"] [dev-dependencies] -dioxus-html = { path = "../html" } dioxus-hooks = { path = "../hooks" } simple_logger = "1.13.0" diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 54b62217a..b6e8ac4c7 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,7 +1,6 @@ use std::cell::RefCell; use dioxus::prelude::Scope; -use dioxus::ScopeChildren; use dioxus_core as dioxus; use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties}; use dioxus_core_macro::Props; @@ -44,7 +43,7 @@ pub struct WebviewWindowProps<'a> { /// focuse me onfocused: &'a dyn FnMut(()), - children: ScopeChildren<'a>, + children: Element, } /// A handle to a @@ -57,7 +56,7 @@ pub struct WebviewWindowProps<'a> { /// /// /// -pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element<'a> { +pub fn WebviewWindow(cx: Context, props: &WebviewWindowProps) -> Element { let dtcx = cx.consume_state::>()?; cx.use_hook( @@ -67,9 +66,6 @@ pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element< |state| { // }, - |hook| { - // - }, ); // render the children directly @@ -94,7 +90,7 @@ fn syntax_works() { use dioxus_hooks::*; use dioxus_html as dioxus_elements; - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx! { // left window WebviewWindow { diff --git a/packages/desktop/src/err.rs b/packages/desktop/src/err.rs index e69de29bb..8b1378917 100644 --- a/packages/desktop/src/err.rs +++ b/packages/desktop/src/err.rs @@ -0,0 +1 @@ + diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 82a698f05..377964b7c 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -4,7 +4,8 @@ use std::sync::Arc; use std::{any::Any, rc::Rc}; -use dioxus_core::{events::on::MouseEvent, ElementId, EventPriority, ScopeId, UserEvent}; +use dioxus_core::{ElementId, EventPriority, ScopeId, UserEvent}; +use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] struct ImEvent { @@ -23,7 +24,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { contents, } = ims.into_iter().next().unwrap(); - let scope = ScopeId(scope as usize); + let scope_id = ScopeId(scope as usize); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); let name = event_name_from_typ(&event); @@ -31,15 +32,14 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { UserEvent { name, - event, - scope, + priority: EventPriority::Low, + scope_id, mounted_dom_id, + event, } } fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box { - use dioxus_core::events::on::*; - match name { "copy" | "cut" | "paste" => { // diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 7484f1bbc..495deebdd 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,7 +13,6 @@ use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; use cfg::DesktopConfig; -use dioxus_core::scheduler::SchedulerMsg; use dioxus_core::*; use serde::{Deserialize, Serialize}; @@ -167,7 +166,7 @@ pub fn run( sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap(); if let Some(BridgeEvent::Update(edits)) = rx.blocking_recv() { - log::info!("bridge received message {:?}", edits); + log::info!("bridge received message"); Some(RpcResponse::new_result(req.id.take(), Some(edits))) } else { log::info!("none received message"); @@ -277,7 +276,7 @@ pub(crate) fn launch_vdom_with_tokio( runtime.block_on(async move { let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver); - let _ = vir.get_event_sender(); + let _ = vir.get_scheduler_channel(); let edits = vir.rebuild(); @@ -299,14 +298,13 @@ pub(crate) fn launch_vdom_with_tokio( // todo: maybe we want to schedule ourselves in // on average though, the virtualdom running natively is stupid fast - let mut muts = vir.run_with_deadline(|| false); + let mut muts = vir.work_with_deadline(|| false); log::debug!("finished running with deadline"); let mut edits = vec![]; while let Some(edit) = muts.pop() { - log::debug!("sending message on channel with edit {:?}", edit); let edit_string = serde_json::to_value(Evt { edits: edit.edits }) .expect("serializing edits should never fail"); edits.push(edit_string); diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index b1febb498..56d0fab77 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -17,6 +17,8 @@ pub(crate) struct ProvidedStateInner { impl ProvidedStateInner { pub(crate) fn notify_consumers(&mut self) { for consumer in self.consumers.iter() { + println!("notifiying {:?}", consumer); + // log::debug("notifiying {:?}", consumer); (self.notify_any)(*consumer); } } @@ -82,13 +84,6 @@ pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option None, } }, - |f| { - // we need to unsubscribe when our component is unounted - if let Some(root) = &f.root { - let mut root = root.borrow_mut(); - root.consumers.remove(&f.scope_id); - } - }, ) } @@ -98,6 +93,15 @@ struct SharedStateInner { scope_id: ScopeId, needs_notification: Cell, } +impl Drop for SharedStateInner { + fn drop(&mut self) { + // we need to unsubscribe when our component is unounted + if let Some(root) = &self.root { + let mut root = root.borrow_mut(); + root.consumers.remove(&self.scope_id); + } + } +} pub struct UseSharedState<'a, T: 'static> { pub(crate) cx: Context<'a>, @@ -172,6 +176,5 @@ pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T) cx.provide_state(state) }, |inner| {}, - |_| {}, ) } diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 0f780b6e4..75ae9c82b 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -16,7 +16,6 @@ pub fn use_ref(cx: Context, f: impl FnOnce() -> T) -> UseRef { inner.update_scheuled.set(false); UseRef { inner } }, - |_| {}, ) } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 76a72ea75..1989fcd20 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -69,7 +69,6 @@ pub fn use_state<'a, T: 'static>( UseState { inner: &*hook } }, - |_| {}, ) } struct UseStateInner { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 20ef160d4..4903276e9 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -9,7 +9,12 @@ description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Vir # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "0.1.2" } +dioxus-core = { path = "../core", version = "0.1.3" } +# Serialize the Edits for use in Webview/Liveview instances +serde = { version = "1", features = ["derive"], optional = true } +serde_repr = { version = "0.1.7", optional = true } -[dev-dependencies] -scraper = "0.12.0" + +[features] +default = [] +serialize = ["serde", "serde_repr"] diff --git a/packages/html/src/attrval.rs b/packages/html/src/attrval.rs deleted file mode 100644 index 9ac1f1373..000000000 --- a/packages/html/src/attrval.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! This module is not included anywhere. -//! -//! It is a prototype for a system that supports non-string attribute values. - -trait AsAttributeValue: Sized { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>; -} -enum AttributeValue<'a> { - Int(i32), - Float(f32), - Str(&'a str), - Bool(bool), -} -impl<'b> AsAttributeValue for Arguments<'b> { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for &'static str { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for f32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for i32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs new file mode 100644 index 000000000..6406e54c8 --- /dev/null +++ b/packages/html/src/elements.rs @@ -0,0 +1,1126 @@ +use crate::{GlobalAttributes, SvgAttributes}; +use dioxus_core::*; +use std::fmt::Arguments; + +macro_rules! builder_constructors { + ( + $( + $(#[$attr:meta])* + $name:ident { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* + }; + )* + ) => { + $( + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl DioxusElement for $name { + const TAG_NAME: &'static str = stringify!($name); + const NAME_SPACE: Option<&'static str> = None; + } + + impl GlobalAttributes for $name {} + + impl $name { + $( + $(#[$attr_method])* + pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr(stringify!($fil), val, None, false) + } + )* + } + )* + }; + + ( $( + $(#[$attr:meta])* + $name:ident <> $namespace:tt { + $($fil:ident: $vil:ident,)* + }; + )* ) => { + $( + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl DioxusElement for $name { + const TAG_NAME: &'static str = stringify!($name); + const NAME_SPACE: Option<&'static str> = Some($namespace); + } + + impl SvgAttributes for $name {} + + impl $name { + $( + pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr(stringify!($fil), val, Some(stringify!($namespace)), false) + } + )* + } + )* + }; +} + +// Organized in the same order as +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +// +// Does not include obsolete elements. +// +// This namespace represents a collection of modern HTML-5 compatiable elements. +// +// This list does not include obsolete, deprecated, experimental, or poorly supported elements. +builder_constructors! { + // Document metadata + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) + /// element. + /// + base { + href: Uri, + target: Target, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) + /// element. + head {}; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + /// element. + link { + // as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + r#type: Mime, + integrity: String, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) + /// element. + meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: HTTPEquiv, + name: Metadata, + }; + + /// Build a + /// [`