Merge pull request #31 from DioxusLabs/jk/stack_dst

stack dst, children as props arg, rename component to scope, rename domtree to element, add router package
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2021-11-11 16:37:33 -05:00
commit af891ac532
112 files changed
+5002 -5798

No files matched your search

+7 -7
View File
@@ -49,7 +49,7 @@
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust. Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust.
```rust ```rust
fn App((cx, props): Scope<()>) -> Element { fn App(cx: Context, props: &()) -> Element {
let mut count = use_state(cx, || 0); let mut count = use_state(cx, || 0);
cx.render(rsx!( 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 | | NodeRef | ✅ | ✅ | gain direct access to nodes |
| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | | 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 | | 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) | | 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 | | Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees |
| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | | Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts |
| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations | | 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 | | Fine-grained reactivity | 👀 | ❓ | Skip diffing for fine-grain updates |
- ✅ = implemented and working - ✅ = implemented and working
+1 -1
View File
@@ -17,7 +17,7 @@ pub static App: FC<()> = |(cx, _)| {
let (async_count, dir) = (count.for_async(), *direction); 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 { loop {
TimeoutFuture::new(250).await; TimeoutFuture::new(250).await;
*async_count.get_mut() += dir; *async_count.get_mut() += dir;
+1 -1
View File
@@ -21,7 +21,7 @@ fn main() {
} }
fn App((cx, props): Scope<()>) -> Element { fn App((cx, props): Scope<()>) -> Element {
let text: &mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); let text: &mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f);
let first = text.get_mut(0).unwrap(); let first = text.get_mut(0).unwrap();
+1 -1
View File
@@ -5,7 +5,7 @@ fn main() {
dom.rebuild(); dom.rebuild();
} }
const App: FC<()> = |(cx, props)| { const App: FC<()> = |cx, props| {
let id = cx.scope_id(); let id = cx.scope_id();
// cx.submit_task(Box::pin(async move { id })); // cx.submit_task(Box::pin(async move { id }));
+1 -1
View File
@@ -6,7 +6,7 @@ struct SomeContext {
} }
#[allow(unused)] #[allow(unused)]
static Example: FC<()> = |(cx, props)| { static Example: FC<()> = |cx, props| {
todo!() todo!()
// let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap()); // let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap());
+3 -3
View File
@@ -33,7 +33,7 @@ fn html_usage() {
static App2: FC<()> = |(cx, _)| cx.render(rsx!("hello world!")); 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); let name = cx.use_state(|| 0);
cx.render(rsx!(div { 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); let name = cx.use_state(|| 0);
cx.render(rsx!( 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<dyn Fn(NodeFactory) -> VNode> = Box::new(|f| todo!()); let b: Box<dyn Fn(NodeFactory) -> VNode> = Box::new(|f| todo!());
+1 -1
View File
@@ -5,7 +5,7 @@ use dioxus_core::{lazynodes::LazyNodes, prelude::*};
// #[async_std::main] // #[async_std::main]
fn main() { fn main() {
static App: FC<()> = 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); let mut dom = VirtualDom::new(App);
+8 -8
View File
@@ -32,7 +32,7 @@ use dioxus::prelude::*;
struct NoKeysProps { struct NoKeysProps {
data: std::collections::HashMap<u32, String>, data: std::collections::HashMap<u32, String>,
} }
static AntipatternNoKeys: FC<NoKeysProps> = |(cx, props)| { static AntipatternNoKeys: FC<NoKeysProps> = |cx, props| {
// WRONG: Make sure to add keys! // WRONG: Make sure to add keys!
rsx!(cx, ul { rsx!(cx, ul {
{props.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} {props.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))}
@@ -54,7 +54,7 @@ static AntipatternNoKeys: FC<NoKeysProps> = |(cx, props)| {
/// ///
/// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing /// 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. /// 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 // Try to avoid heavily nesting fragments
rsx!(cx, rsx!(cx,
Fragment { 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 /// 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` /// 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. /// 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(); let (state, set_state) = use_state(cx, || "Hello world").classic();
set_state("New state"); set_state("New state");
// This will return false! `state` will *still* be "Hello world" // 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 /// - 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 /// 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 /// Antipattern: Misusing hooks
/// --------------------------- /// ---------------------------
@@ -120,7 +120,7 @@ static antipattern_component: FC<()> = |(cx, props)| todo!();
struct MisuedHooksProps { struct MisuedHooksProps {
should_render_state: bool, should_render_state: bool,
} }
static AntipatternMisusedHooks: FC<MisuedHooksProps> = |(cx, props)| { static AntipatternMisusedHooks: FC<MisuedHooksProps> = |cx, props| {
if props.should_render_state { if props.should_render_state {
// do not place a hook in the conditional! // do not place a hook in the conditional!
// prefer to move it out of the conditional // prefer to move it out of the conditional
@@ -153,7 +153,7 @@ static AntipatternMisusedHooks: FC<MisuedHooksProps> = |(cx, props)| {
/// } /// }
/// } /// }
/// }) /// })
static _example: FC<()> = |(cx, props)| todo!(); static _example: FC<()> = |cx, props| todo!();
/// Antipattern: publishing components and hooks with all features enabled /// 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 /// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific
/// libraries. /// 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! { cx.render(rsx! {
AntipatternNoKeys { data: std::collections::HashMap::new() } AntipatternNoKeys { data: std::collections::HashMap::new() }
AntipatternNestedFragments {} AntipatternNestedFragments {}
+2 -2
View File
@@ -9,7 +9,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
Greeting { Greeting {
@@ -25,7 +25,7 @@ struct GreetingProps {
name: &'static str, name: &'static str,
} }
static Greeting: FC<GreetingProps> = |(cx, props)| { static Greeting: FC<GreetingProps> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "Hello, {props.name}!" } h1 { "Hello, {props.name}!" }
+2 -2
View File
@@ -18,7 +18,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
Banner { 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! { cx.render(rsx! {
div { div {
h1 { "This is a great banner!" } h1 { "This is a great banner!" }
@@ -16,7 +16,7 @@ use dioxus::prelude::*;
pub struct MyProps { pub struct MyProps {
should_show: bool, should_show: bool,
} }
pub static Example0: FC<MyProps> = |(cx, props)| { pub static Example0: FC<MyProps> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
{props.should_show.then(|| rsx!{ {props.should_show.then(|| rsx!{
@@ -39,7 +39,7 @@ pub static Example0: FC<MyProps> = |(cx, props)| {
pub struct MyProps1 { pub struct MyProps1 {
should_show: bool, should_show: bool,
} }
pub static Example1: FC<MyProps1> = |(cx, props)| { pub static Example1: FC<MyProps1> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
// With matching // With matching
@@ -77,7 +77,7 @@ pub enum Color {
pub struct MyProps2 { pub struct MyProps2 {
color: Color, color: Color,
} }
pub static Example2: FC<MyProps2> = |(cx, props)| { pub static Example2: FC<MyProps2> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
{match props.color { {match props.color {
@@ -89,7 +89,7 @@ pub static Example2: FC<MyProps2> = |(cx, props)| {
}) })
}; };
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let should_show = use_state(cx, || false); let should_show = use_state(cx, || false);
let mut color_index = use_state(cx, || 0); let mut color_index = use_state(cx, || 0);
let color = match *color_index % 2 { let color = match *color_index % 2 {
+3 -3
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() {} fn main() {}
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
@@ -10,7 +10,7 @@ pub static Example: FC<()> = |(cx, props)| {
}; };
// A controlled component: // A controlled component:
static ControlledSelect: FC<()> = |(cx, props)| { static ControlledSelect: FC<()> = |cx, props| {
let value = use_state(cx, || String::from("Grapefruit")); let value = use_state(cx, || String::from("Grapefruit"));
cx.render(rsx! { cx.render(rsx! {
select { value: "{value}", onchange: move |evt| value.set(evt.value()), 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? // TODO - how do uncontrolled things work?
static UncontrolledSelect: FC<()> = |(cx, props)| { static UncontrolledSelect: FC<()> = |cx, props| {
let value = use_state(cx, || String::new()); let value = use_state(cx, || String::new());
cx.render(rsx! { cx.render(rsx! {
+1 -1
View File
@@ -11,7 +11,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
custom_element { custom_element {
+1 -1
View File
@@ -5,4 +5,4 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| cx.render(rsx! { Fragment {} }); pub static Example: FC<()> = |cx, props| cx.render(rsx! { Fragment {} });
+6 -6
View File
@@ -23,14 +23,14 @@ fn main() {}
/// This is one way to go about error handling (just toss things away with unwrap). /// 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. /// However, if you get it wrong, the whole app will crash.
/// This is pretty flimsy. /// This is pretty flimsy.
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let data = get_data().unwrap(); let data = get_data().unwrap();
cx.render(rsx!( div { "{data}" } )) cx.render(rsx!( div { "{data}" } ))
}; };
/// This is a pretty verbose way of error handling /// 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 /// 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() { let data = match get_data() {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
@@ -46,7 +46,7 @@ static App1: FC<()> = |(cx, props)| {
/// a user is logged in. /// a user is logged in.
/// ///
/// Dioxus will throw an error in the console if the None-path is ever taken. /// 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()?; let data = get_data()?;
cx.render(rsx!( div { "{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. /// This is top-tier error handling since it displays a failure state.
/// ///
/// However, the error is lacking in context. /// 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}" } )), Some(data) => cx.render(rsx!( div { "{data}" } )),
None => cx.render(rsx!( div { "Failed to load 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 /// For errors that return results, it's possible to short-circuit the match-based error handling with `.ok()` which converts
/// a Result<T, V> into an Option<T> and lets you abort rendering by early-returning `None` /// a Result<T, V> into an Option<T> 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()?; let data = get_data_err().ok()?;
cx.render(rsx!( div { "{data}" } )) 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! /// 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 /// 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}" } )), Ok(data) => cx.render(rsx!( div { "{data}" } )),
Err(c) => cx.render(rsx!( div { "Failed to load data: {c}" } )), Err(c) => cx.render(rsx!( div { "Failed to load data: {c}" } )),
}; };
+4 -4
View File
@@ -11,7 +11,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
// Returning multiple elements with rsx! or html! // Returning multiple elements with rsx! or html!
static App1: FC<()> = |(cx, props)| { static App1: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
h1 { } h1 { }
h2 { } h2 { }
@@ -20,7 +20,7 @@ static App1: FC<()> = |(cx, props)| {
}; };
// Using the Fragment component // Using the Fragment component
static App2: FC<()> = |(cx, props)| { static App2: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
Fragment { Fragment {
div {} div {}
@@ -31,7 +31,7 @@ static App2: FC<()> = |(cx, props)| {
}; };
// Using the `fragment` method on the NodeFactory // Using the `fragment` method on the NodeFactory
static App3: FC<()> = |(cx, props)| { static App3: FC<()> = |cx, props| {
cx.render(LazyNodes::new(move |fac| { cx.render(LazyNodes::new(move |fac| {
fac.fragment_from_iter([ fac.fragment_from_iter([
fac.text(format_args!("A")), 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! { cx.render(rsx! {
App1 {} App1 {}
App2 {} App2 {}
+1 -1
View File
@@ -19,7 +19,7 @@ h1 {color: blue;}
p {color: red;} p {color: red;}
"#; "#;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
head { style { "{STYLE}" } } head { style { "{STYLE}" } }
body { body {
+2 -2
View File
@@ -10,7 +10,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
head { head {
style: { background_color: "powderblue" } 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 {} // .... 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.. // I haven't noticed any name collisions yet, and am tentatively leaving this behavior in..
// Don't rely on it. // Don't rely on it.
static Example2: FC<()> = |(cx, props)| { static Example2: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { color: "red" div { color: "red"
"hello world!" "hello world!"
+1 -1
View File
@@ -12,7 +12,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let example_data = use_state(cx, || 0); let example_data = use_state(cx, || 0);
let v = (0..10).map(|f| { let v = (0..10).map(|f| {
+4 -4
View File
@@ -7,7 +7,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
ButtonList {} ButtonList {}
NonUpdatingEvents {} 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. /// 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 name = use_state(cx, || "...?");
let names = ["jack", "jill", "john", "jane"] 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. /// This shows how listeners may be without a visible change in the display.
/// Check the console. /// Check the console.
static NonUpdatingEvents: FC<()> = |(cx, props)| { static NonUpdatingEvents: FC<()> = |cx, props| {
rsx!(cx, div { rsx!(cx, div {
button { button {
onclick: move |_| log::info!("Did not cause any updates!") 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, rsx!(cx,
div { div {
onclick: move |_| log::info!("event propagated to the div!") onclick: move |_| log::info!("event propagated to the div!")
+3 -3
View File
@@ -21,7 +21,7 @@ use dioxus::prelude::*;
// By default, components with no props are always memoized. // By default, components with no props are always memoized.
// A props of () is considered empty. // A props of () is considered empty.
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { "100% memoized!" } div { "100% memoized!" }
}) })
@@ -35,7 +35,7 @@ pub struct MyProps1 {
name: String, name: String,
} }
pub static Example1: FC<MyProps1> = |(cx, props)| { pub static Example1: FC<MyProps1> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { "100% memoized! {props.name}" } div { "100% memoized! {props.name}" }
}) })
@@ -49,7 +49,7 @@ pub struct MyProps2 {
name: std::rc::Rc<str>, name: std::rc::Rc<str>,
} }
pub static Example2: FC<MyProps2> = |(cx, props)| { pub static Example2: FC<MyProps2> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { "100% memoized! {props.name}" } div { "100% memoized! {props.name}" }
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() {} fn main() {}
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let p = 10; let p = 10;
cx.render(rsx! { cx.render(rsx! {
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() {} fn main() {}
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
+1 -1
View File
@@ -9,7 +9,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let props = MyProps { let props = MyProps {
count: 0, count: 0,
live: true, live: true,
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() {} fn main() {}
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
+1 -1
View File
@@ -14,7 +14,7 @@ struct DogApi {
} }
const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random"; 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( let doggo = use_suspense(
cx, cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(), || surf::get(ENDPOINT).recv_json::<DogApi>(),
+1 -1
View File
@@ -24,7 +24,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let count = use_state(cx, || 0); let count = use_state(cx, || 0);
let mut direction = use_state(cx, || 1); let mut direction = use_state(cx, || 1);
+1 -1
View File
@@ -1,6 +1,6 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
+2 -2
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::ssr; use dioxus::ssr;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let as_string = use_state(cx, || { let as_string = use_state(cx, || {
// Currently, SSR is only supported for whole VirtualDOMs // Currently, SSR is only supported for whole VirtualDOMs
// This is an easy/low hanging fruit to improve upon // 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! { cx.render(rsx! {
div { style: {background_color: "blue"} div { style: {background_color: "blue"}
h1 {"Some amazing app or component"} h1 {"Some amazing app or component"}
+3 -3
View File
@@ -27,18 +27,18 @@ fn main() {
use dioxus::prelude::*; use dioxus::prelude::*;
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let p1 = use_state(cx, || 0); let p1 = use_state(cx, || 0);
let p2 = use_state(cx, || 0); let p2 = use_state(cx, || 0);
let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async()); 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 { loop {
*p1_async.get_mut() += 1; *p1_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(75)).await; 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 { loop {
*p2_async.get_mut() += 1; *p2_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(100)).await; async_std::task::sleep(std::time::Duration::from_millis(100)).await;
+13 -11
View File
@@ -90,15 +90,17 @@ static App: FC<()> = |(cx, _)| {
} }
}; };
rsx!(cx, body { cx.render(rsx!(
link { body {
rel: "stylesheet" link {
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" rel: "stylesheet"
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css"
crossorigin: "anonymous" integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5"
} crossorigin: "anonymous"
margin_left: "35%" }
h1 {"Dioxus CRM Example"} margin_left: "35%"
{scene} h1 {"Dioxus CRM Example"}
}) {scene}
}
))
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ fn main() {
dioxus_desktop::launch(App, |c| c); dioxus_desktop::launch(App, |c| c);
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { div {
"hello world!" "hello world!"
+1 -1
View File
@@ -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 file_manager = use_ref(cx, || Files::new());
let files = file_manager.read(); let files = file_manager.read();
+1 -1
View File
@@ -19,7 +19,7 @@ fn main() {
dioxus::desktop::launch(App, |c| c.with_prerendered(content)); 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); let mut val = use_state(cx, || 0);
cx.render(rsx! { cx.render(rsx! {
+1 -1
View File
@@ -31,7 +31,7 @@ fn main() {
}); });
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let state = use_ref(cx, || Calculator::new()); let state = use_ref(cx, || Calculator::new());
let clear_display = state.read().display_value.eq("0"); let clear_display = state.read().display_value.eq("0");
+1 -1
View File
@@ -7,7 +7,7 @@ fn main() {
dioxus::desktop::launch(App, |c| c); dioxus::desktop::launch(App, |c| c);
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0); let mut count = use_state(cx, || 0);
cx.render(rsx! { cx.render(rsx! {
+1 -1
View File
@@ -23,7 +23,7 @@ pub enum Route {
NotFound, NotFound,
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let route = use_router(cx, Route::parse)?; let route = use_router(cx, Route::parse)?;
cx.render(rsx! { cx.render(rsx! {
+10 -6
View File
@@ -49,7 +49,7 @@ const NONE_ELEMENT: Option<()> = None;
use baller::Baller; use baller::Baller;
use dioxus::prelude::*; use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| { pub static Example: FC<()> = |cx, props| {
let formatting = "formatting!"; let formatting = "formatting!";
let formatting_tuple = ("a", "b"); let formatting_tuple = ("a", "b");
let lazy_fmt = format_args!("lazily formatted text"); let lazy_fmt = format_args!("lazily formatted text");
@@ -102,12 +102,9 @@ pub static Example: FC<()> = |(cx, props)| {
}} }}
// Matching // 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 { {match true {
true => rsx!(cx, h1 {"Top text"}), true => rsx!( h1 {"Top text"}),
false => cx.render(rsx!( h1 {"Bottom text"})) false => rsx!( h1 {"Bottom text"})
}} }}
// Conditional rendering // Conditional rendering
@@ -174,10 +171,17 @@ pub static Example: FC<()> = |(cx, props)| {
// Can take children too! // Can take children too!
Taller { a: "asd", div {"hello world!"} } 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 { mod baller {
use super::*; use super::*;
#[derive(Props, PartialEq)] #[derive(Props, PartialEq)]
+2 -2
View File
@@ -9,7 +9,7 @@ fn main() {
println!("{}", ssr::render_vdom(&vdom, |c| c)); println!("{}", ssr::render_vdom(&vdom, |c| c));
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { div {
h1 { "Title" } h1 { "Title" }
@@ -21,6 +21,6 @@ static App: FC<()> = |(cx, props)| {
struct MyProps<'a> { struct MyProps<'a> {
text: &'a str, text: &'a str,
} }
fn App2<'a>(cx: Context<'a>, props: &'a MyProps) -> Element<'a> { fn App2(cx: Context, props: &MyProps) -> Element {
None None
} }
+1 -1
View File
@@ -12,7 +12,7 @@ fn main() {
) )
} }
pub static App: FC<()> = |(cx, props)| { pub static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { div {
class: "overflow-hidden" class: "overflow-hidden"
+1 -1
View File
@@ -42,7 +42,7 @@ fn main() {}
// initial_name: String, // initial_name: String,
// } // }
// static Example: FC<ExampleProps> = |(cx, props)| { // static Example: FC<ExampleProps> = |cx, props| {
// let dispaly_name = use_state(cx, move || props.initial_name.clone()); // let dispaly_name = use_state(cx, move || props.initial_name.clone());
// cx.render(rsx! { // cx.render(rsx! {
+6 -6
View File
@@ -24,7 +24,7 @@ fn main() {
.unwrap(); .unwrap();
} }
pub static App: FC<()> = |(cx, props)| { pub static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { class: "overflow-hidden" div { class: "overflow-hidden"
link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } 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! { cx.render(rsx! {
div { div {
header { class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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!( cx.render(rsx!(
svg { svg {
xmlns: "http://www.w3.org/2000/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!( cx.render(rsx!(
svg { svg {
fill: "none" fill: "none"
+6 -6
View File
@@ -14,7 +14,7 @@ fn main() {
const STYLE: &str = "body {overflow:hidden;}"; const STYLE: &str = "body {overflow:hidden;}";
pub static App: FC<()> = |(cx, props)| { pub static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { class: "overflow-hidden" div { class: "overflow-hidden"
style { "{STYLE}" } 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! { cx.render(rsx! {
div { div {
header { class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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!( cx.render(rsx!(
svg { svg {
// xmlns: "http://www.w3.org/2000/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!( cx.render(rsx!(
svg { svg {
fill: "none" fill: "none"
+23
View File
@@ -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}" }
}
})
};
+2 -2
View File
@@ -22,7 +22,7 @@ pub struct TodoItem {
} }
const STYLE: &str = include_str!("./assets/todomvc.css"); 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 draft = use_state(cx, || "".to_string());
let mut todos = use_state(cx, || HashMap::<u32, Rc<TodoItem>>::new()); let mut todos = use_state(cx, || HashMap::<u32, Rc<TodoItem>>::new());
let mut filter = use_state(cx, || FilterState::All); let mut filter = use_state(cx, || FilterState::All);
@@ -85,7 +85,7 @@ pub struct TodoEntryProps {
todo: Rc<TodoItem>, todo: Rc<TodoItem>,
} }
pub fn TodoEntry((cx, props): Scope<TodoEntryProps>) -> Element { pub fn TodoEntry(cx: Context, props: &TodoEntryProps) -> Element {
let mut is_editing = use_state(cx, || false); let mut is_editing = use_state(cx, || false);
let mut contents = use_state(cx, || String::from("")); let mut contents = use_state(cx, || String::from(""));
let todo = &props.todo; let todo = &props.todo;
+8 -6
View File
@@ -12,7 +12,7 @@ fn main() {
const ENDPOINT: &str = "https://api.openweathermap.org/data/2.5/weather"; 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( let body = use_suspense(
cx, cx,
@@ -24,21 +24,23 @@ static App: FC<()> = |(cx, props)| {
.await .await
.unwrap(); .unwrap();
}, },
|cx, props| { |props| {
// //
rsx!(WeatherDisplay {}) cx.render(rsx!(WeatherDisplay {}))
}, },
); );
rsx!(cx, div { cx.render(rsx! {
{body} div {
{body}
}
}) })
}; };
#[derive(PartialEq, Props)] #[derive(PartialEq, Props)]
struct WeatherProps {} struct WeatherProps {}
static WeatherDisplay: FC<WeatherProps> = |(cx, props)| { static WeatherDisplay: FC<WeatherProps> = |cx, props| {
// //
cx.render(rsx!( cx.render(rsx!(
div { class: "flex items-center justify-center flex-col" div { class: "flex items-center justify-center flex-col"
+1 -1
View File
@@ -24,7 +24,7 @@ fn main() {
dioxus_web::launch(App, |c| c) dioxus_web::launch(App, |c| c)
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let mut state = use_state(cx, || 0); let mut state = use_state(cx, || 0);
cx.render(rsx! { cx.render(rsx! {
div { div {
+2 -2
View File
@@ -23,7 +23,7 @@ fn main() {
// dioxus::web::launch(App, |c| c); // dioxus::web::launch(App, |c| c);
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
dbg!("rednering parent"); dbg!("rednering parent");
cx.render(rsx! { cx.render(rsx! {
div { 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 mut count = use_state(cx, || 0);
// let d = Dropper { name: "asd" }; // let d = Dropper { name: "asd" };
+6 -6
View File
@@ -24,7 +24,7 @@ fn main() {
dioxus_web::launch(App, |c| c) dioxus_web::launch(App, |c| c)
} }
pub static App: FC<()> = |(cx, props)| { pub static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div { class: "overflow-hidden" div { class: "overflow-hidden"
link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } 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! { cx.render(rsx! {
div { div {
header { class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" 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!( cx.render(rsx!(
svg { svg {
// xmlns: "http://www.w3.org/2000/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!( cx.render(rsx!(
svg { svg {
fill: "none" fill: "none"
+1 -1
View File
@@ -19,7 +19,7 @@ fn main() {
dioxus::web::launch(App, |c| c); dioxus::web::launch(App, |c| c);
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let mut rng = SmallRng::from_entropy(); let mut rng = SmallRng::from_entropy();
let rows = (0..1_000).map(|f| { let rows = (0..1_000).map(|f| {
let label = Label::new(&mut rng); let label = Label::new(&mut rng);
+1 -1
View File
@@ -16,7 +16,7 @@ fn main() {
dioxus::web::launch(App, |c| c); dioxus::web::launch(App, |c| c);
} }
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0); let mut count = use_state(cx, || 0);
cx.render(rsx! { cx.render(rsx! {
+1 -1
View File
@@ -173,7 +173,7 @@ impl ToTokens for Component {
if !self.children.is_empty() { if !self.children.is_empty() {
let childs = &self.children; let childs = &self.children;
toks.append_all(quote! { toks.append_all(quote! {
.children(ScopeChildren::new(__cx.fragment_from_iter([ #( #childs ),* ]))) .children(__cx.create_children([ #( #childs ),* ]))
}); });
} }
+2 -2
View File
@@ -210,12 +210,12 @@ impl ToTokens for ElementAttrNamed {
ElementAttr::EventClosure { name, closure } => { ElementAttr::EventClosure { name, closure } => {
quote! { quote! {
dioxus::events::on::#name(__cx, #closure) dioxus_elements::on::#name(__cx, #closure)
} }
} }
ElementAttr::EventTokens { name, tokens } => { ElementAttr::EventTokens { name, tokens } => {
quote! { quote! {
dioxus::events::on::#name(__cx, #tokens) dioxus_elements::on::#name(__cx, #tokens)
} }
} }
}; };
+2 -3
View File
@@ -40,8 +40,6 @@ indexmap = "1.7.0"
# Serialize the Edits for use in Webview/Liveview instances # Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1.7", optional = true }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.42" anyhow = "1.0.42"
dioxus-html = { path = "../html" } dioxus-html = { path = "../html" }
@@ -49,12 +47,13 @@ fern = { version = "0.6.0", features = ["colored"] }
rand = { version = "0.8.4", features = ["small_rng"] } rand = { version = "0.8.4", features = ["small_rng"] }
simple_logger = "1.13.0" simple_logger = "1.13.0"
dioxus-core-macro = { path = "../core-macro", version = "0.1.2" } dioxus-core-macro = { path = "../core-macro", version = "0.1.2" }
dioxus-hooks = { path = "../hooks" }
# async-std = { version = "1.9.0", features = ["attributes"] } # async-std = { version = "1.9.0", features = ["attributes"] }
# criterion = "0.3.5" # criterion = "0.3.5"
[features] [features]
default = [] default = []
serialize = ["serde", "serde_repr"] serialize = ["serde"]
debug_vdom = [] debug_vdom = []
[[bench]] [[bench]]
+16
View File
@@ -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 "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. - 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.
+102
View File
@@ -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 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 10. return the patches back to the event loop
11. apply the patches to the real dom 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?
+2 -2
View File
@@ -24,7 +24,7 @@ criterion_group!(mbenches, create_rows);
criterion_main!(mbenches); criterion_main!(mbenches);
fn create_rows(c: &mut Criterion) { fn create_rows(c: &mut Criterion) {
static App: FC<()> = |(cx, _)| { static App: FC<()> = |cx, _| {
let mut rng = SmallRng::from_entropy(); let mut rng = SmallRng::from_entropy();
let rows = (0..10_000_usize).map(|f| { let rows = (0..10_000_usize).map(|f| {
let label = Label::new(&mut rng); let label = Label::new(&mut rng);
@@ -58,7 +58,7 @@ struct RowProps {
row_id: usize, row_id: usize,
label: Label, label: Label,
} }
fn Row((cx, props): Scope<RowProps>) -> Element { fn Row(cx: Context, props: &RowProps) -> Element {
let [adj, col, noun] = props.label.0; let [adj, col, noun] = props.label.0;
cx.render(rsx! { cx.render(rsx! {
tr { tr {
+9
View File
@@ -0,0 +1,9 @@
use dioxus_core as dioxus;
use dioxus_core_macro::*;
fn main() {}
#[derive(Props)]
struct ChildProps<'a> {
name: &'a str,
}
+44
View File
@@ -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}!" }
})
}
-124
View File
@@ -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<usize>,
// 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);
}
}
-93
View File
@@ -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
}
}
+65 -73
View File
@@ -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 //! 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. //! that ensures compile-time required and optional fields on cx.
use crate::{ use crate::innerlude::{Context, Element, LazyNodes, NodeLink};
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<State>) -> 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);
pub struct FragmentProps<'a> { pub struct FragmentProps(Element);
children: ScopeChildren<'a>, pub struct FragmentBuilder<const BUILT: bool>(Element);
impl FragmentBuilder<false> {
pub fn children(self, children: Option<NodeLink>) -> FragmentBuilder<true> {
FragmentBuilder(children)
}
} }
impl<const A: bool> FragmentBuilder<A> {
pub struct FragmentBuilder<'a, const BUILT: bool> { pub fn build(self) -> FragmentProps {
children: Option<ScopeChildren<'a>>, FragmentProps(self.0)
}
impl<'a> FragmentBuilder<'a, false> {
pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> {
FragmentBuilder {
children: Some(children),
}
} }
} }
impl<'a, const A: bool> FragmentBuilder<'a, A> { /// Access the children elements passed into the component
pub fn build(self) -> FragmentProps<'a> { ///
FragmentProps { /// This enables patterns where a component is passed children from its parent.
children: self.children.unwrap_or_default(), ///
} /// ## 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
impl<'a> Properties for FragmentProps<'a> { /// on the props that takes Context.
type Builder = FragmentBuilder<'a, false>; ///
/// 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<false>;
const IS_STATIC: bool = false; const IS_STATIC: bool = false;
fn builder() -> Self::Builder { fn builder() -> Self::Builder {
FragmentBuilder { children: None } FragmentBuilder(None)
} }
unsafe fn memoize(&self, _other: &Self) -> bool {
unsafe fn memoize(&self, other: &Self) -> bool {
false false
} }
} }
/// Create inline fragments using Component syntax. /// Create inline fragments using Component syntax.
/// ///
/// ## Details
///
/// Fragments capture a series of children without rendering extra nodes. /// 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 /// ```rust
/// rsx!{ /// 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. /// 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. /// 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. /// 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)] #[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>((cx, props): Scope<'a, FragmentProps<'a>>) -> Element<'a> { pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
cx.render(Some(LazyNodes::new(|f| { cx.render(Some(LazyNodes::new(|f| f.fragment_from_iter(&props.0))))
f.fragment_from_iter(&props.children)
})))
} }
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus /// 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. // that the macros use to anonymously complete prop construction.
pub struct EmptyBuilder; pub struct EmptyBuilder;
impl EmptyBuilder { impl EmptyBuilder {
#[inline]
pub fn build(self) {} pub fn build(self) {}
} }
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props. /// 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() T::builder()
} }
-271
View File
@@ -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<Props>) -> VNode {
/// html! {
/// <div> "Hello, {cx.name}" </div>
/// }
/// }
/// ```
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<dyn Fn() + 'static> {
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<dyn Fn(ScopeId)> {
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! {<div> "Hello World" </div>};
///
/// // Actually build the tree and allocate it
/// cx.render(lazy_tree)
/// }
///```
pub fn render(self, lazy_nodes: Option<LazyNodes<'src, '_>>) -> Option<VNode<'src>> {
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::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
pub fn provide_state<T>(self, value: T)
where
T: 'static,
{
self.scope
.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value))
.map(|f| f.downcast::<T>().ok())
.flatten();
}
/// Try to retrieve a SharedState with type T from the any parent Scope.
pub fn consume_state<T: 'static>(self) -> Option<Rc<T>> {
let getter = &self.scope.shared.get_shared_context;
let ty = TypeId::of::<T>();
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<u32> {
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<T: 'static>(initial_value: impl FnOnce() -> T) -> &RefCell<T> {
/// use_hook(
/// || Rc::new(RefCell::new(initial_value())),
/// |state| state,
/// |_| {},
/// )
/// }
/// ```
pub fn use_hook<State, Output, Init, Run, Cleanup>(
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<State>) + 'static,
{
// If the idx is the same as the hook length, then we need to add the current hook
if self.scope.hooks.at_end() {
self.scope.hooks.push_hook(
initializer(self.scope.hooks.len()),
Box::new(|raw| {
//
let s = raw.downcast::<State>().unwrap();
cleanup(s);
}),
);
}
runner(self.scope.hooks.next::<State>().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.
"###;
-128
View File
@@ -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(())
}
}
+327 -179
View File
@@ -90,6 +90,7 @@
use crate::innerlude::*; use crate::innerlude::*;
use fxhash::{FxHashMap, FxHashSet}; use fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
use DomEdit::*; use DomEdit::*;
/// Our DiffMachine is an iterative tree differ. /// 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 /// 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! /// stack machines all the way down!
pub(crate) struct DiffMachine<'bump> { pub struct DiffState<'bump> {
pub vdom: &'bump ResourcePool, scopes: &'bump ScopeArena,
pub mutations: Mutations<'bump>, pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>, pub(crate) stack: DiffStack<'bump>,
pub seen_scopes: FxHashSet<ScopeId>, pub seen_scopes: FxHashSet<ScopeId>,
pub cfg: DiffCfg,
}
pub(crate) struct DiffCfg {
pub force_diff: bool, pub force_diff: bool,
} }
impl Default for DiffCfg {
fn default() -> Self { impl<'bump> DiffState<'bump> {
pub(crate) fn new(scopes: &'bump ScopeArena) -> Self {
Self { Self {
force_diff: Default::default(), scopes,
} mutations: Mutations::new(),
}
}
/// 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<ScopeId>,
}
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(),
stack: DiffStack::new(), stack: DiffStack::new(),
vdom: shared, seen_scopes: Default::default(),
seen_scopes: FxHashSet::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<DiffInstruction<'bump>>,
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> { pub fn pop(&mut self) -> Option<DiffInstruction<'bump>> {
SavedDiffWork { self.instructions.pop()
mutations: self.mutations, }
stack: self.stack,
seen_scopes: self.seen_scopes, 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) { pub fn push_subtree(&mut self) {
if let Some(component) = self.vdom.get_scope_mut(id) { self.nodes_created_stack.push(0);
let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); self.instructions.push(DiffInstruction::Mount {
self.stack.push(DiffInstruction::Diff { new, old }); and: MountType::Append,
self.work(|| false); });
} }
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<ScopeId> {
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" /// Progress the diffing for this "fiber"
@@ -189,7 +258,10 @@ impl<'bump> DiffMachine<'bump> {
DiffInstruction::Diff { old, new } => self.diff_node(old, new), DiffInstruction::Diff { old, new } => self.diff_node(old, new),
DiffInstruction::Create { node } => self.create_node(node), DiffInstruction::Create { node } => self.create_node(node),
DiffInstruction::Mount { and } => self.mount(and), 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(), DiffInstruction::PopScope => self.stack.pop_off_scope(),
}; };
@@ -202,10 +274,38 @@ impl<'bump> DiffMachine<'bump> {
true true
} }
fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) { // recursively push all the nodes of a tree onto the stack and return how many are there
for el in RealChildIterator::new(node, self.vdom) { fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize {
self.mutations.push_root(el.mounted_id()); match node {
self.stack.add_child_count(1); 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 } => { MountType::Replace { old } => {
if let Some(old_id) = old.try_mounted_id() { self.replace_node(old, nodes_created);
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);
}
} }
MountType::Append => { MountType::Append => {
@@ -260,18 +352,19 @@ impl<'bump> DiffMachine<'bump> {
VNode::Element(element) => self.create_element_node(element, node), VNode::Element(element) => self.create_element_node(element, node),
VNode::Fragment(frag) => self.create_fragment_node(frag), VNode::Fragment(frag) => self.create_fragment_node(frag),
VNode::Component(component) => self.create_component_node(component), 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>) { 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); self.mutations.create_text_node(vtext.text, real_id);
vtext.dom_id.set(Some(real_id)); vtext.dom_id.set(Some(real_id));
self.stack.add_child_count(1); self.stack.add_child_count(1);
} }
fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) { 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); self.mutations.create_placeholder(real_id);
suspended.dom_id.set(Some(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>) { 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); self.mutations.create_placeholder(real_id);
anchor.dom_id.set(Some(real_id)); anchor.dom_id.set(Some(real_id));
self.stack.add_child_count(1); self.stack.add_child_count(1);
} }
@@ -298,7 +393,7 @@ impl<'bump> DiffMachine<'bump> {
.. ..
} = element; } = element;
let real_id = self.vdom.reserve_node(node); let real_id = self.scopes.reserve_node(node);
dom_id.set(Some(real_id)); dom_id.set(Some(real_id));
@@ -307,13 +402,13 @@ impl<'bump> DiffMachine<'bump> {
self.stack.add_child_count(1); self.stack.add_child_count(1);
if let Some(cur_scope_id) = self.stack.current_scope() { 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); self.attach_listener_to_scope(listener, scope);
listener.mounted_node.set(Some(real_id)); listener.mounted_node.set(Some(real_id));
self.mutations.new_event_listener(listener, cur_scope_id); self.mutations.new_event_listener(listener, cur_scope_id);
}); }
} else { } else {
log::warn!("create element called with no scope on the stack - this is an error for a live dom"); 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>) { fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) {
let caller = vcomponent.caller;
let parent_idx = self.stack.current_scope().unwrap(); let parent_idx = self.stack.current_scope().unwrap();
let shared = self.vdom.channel.clone();
// Insert a new scope into our component list // 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| { let parent_scope = unsafe { self.scopes.get_scope_raw(&parent_idx) };
ScopeInner::new( let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) };
caller, let fc_ptr = vcomponent.user_fc;
new_idx,
Some(parent_idx), let new_idx = self
parent_scope.height + 1, .scopes
parent_scope.subtree(), .new_with_key(fc_ptr, caller, parent_scope, height, subtree);
shared,
)
});
// Actually initialize the caller's slot with the right address // Actually initialize the caller's slot with the right address
vcomponent.associated_scope.set(Some(new_idx)); vcomponent.associated_scope.set(Some(new_idx));
if !vcomponent.can_memoize { if !vcomponent.can_memoize {
let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap(); let cur_scope = self.scopes.get_scope(&parent_idx).unwrap();
let extended = vcomponent as *const VComponent; let extended = unsafe { std::mem::transmute(vcomponent) };
let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) }; cur_scope.items.borrow_mut().borrowed_props.push(extended);
cur_scope.borrowed_props.borrow_mut().push(extended);
} }
// TODO: // TODO:
// add noderefs to current noderef list Noderefs // add noderefs to current noderef list Noderefs
// add effects to current effect list Effects // add effects to current effect list Effects
let new_component = self.scopes.get_scope(&new_idx).unwrap();
let new_component = self.vdom.get_scope_mut(new_idx).unwrap();
log::debug!( log::debug!(
"initializing component {:?} with height {:?}", "initializing component {:?} with height {:?}",
new_idx, new_idx,
parent_scope.height + 1 height + 1
); );
// Run the scope for one iteration to initialize it // 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 // 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); self.stack.create_component(new_idx, nextnode);
//
/*
tree_item {
}
*/
if new_component.is_subtree_root.get() { if new_component.is_subtree_root.get() {
self.stack.push_subtree(); self.stack.push_subtree();
} }
@@ -396,6 +477,16 @@ impl<'bump> DiffMachine<'bump> {
self.seen_scopes.insert(new_idx); 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 // Tools for diffing nodes
// ================================= // =================================
@@ -412,11 +503,14 @@ impl<'bump> DiffMachine<'bump> {
(Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()),
(Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new), (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new),
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node), (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 // Anything else is just a basic replace and create
( (
Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), | Suspended(_),
Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
| Suspended(_),
) => self ) => self
.stack .stack
.create_node(new_node, MountType::Replace { old: old_node }), .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 // TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.stack.current_scope() { if let Some(cur_scope_id) = self.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() { if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
@@ -533,33 +627,42 @@ impl<'bump> DiffMachine<'bump> {
&mut self, &mut self,
old_node: &'bump VNode<'bump>, old_node: &'bump VNode<'bump>,
new_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>,
old: &'bump VComponent<'bump>, old: &'bump VComponent<'bump>,
new: &'bump VComponent<'bump>, new: &'bump VComponent<'bump>,
) { ) {
let scope_addr = old.associated_scope.get().unwrap(); 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) // Make sure we're dealing with the same component (by function pointer)
if old.user_fc == new.user_fc { if old.user_fc == new.user_fc {
log::debug!("Diffing component {:?} - {:?}", new.user_fc, scope_addr);
//
self.stack.scope_stack.push(scope_addr); self.stack.scope_stack.push(scope_addr);
// Make sure the new component vnode is referencing the right scope id // Make sure the new component vnode is referencing the right scope id
new.associated_scope.set(Some(scope_addr)); new.associated_scope.set(Some(scope_addr));
// make sure the component's caller function is up to date // make sure the component's caller function is up to date
let scope = self.vdom.get_scope_mut(scope_addr).unwrap(); let scope = unsafe {
scope.update_scope_dependencies(new.caller); 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. // React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap(); let props_are_the_same = old.comparator.unwrap();
if self.cfg.force_diff || !props_are_the_same(new) { if self.force_diff || !props_are_the_same(new) {
let succeeded = scope.run_scope(self.vdom); if self.scopes.run_scope(&scope_addr) {
self.diff_node(
if succeeded { self.scopes.wip_head(&scope_addr),
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); self.scopes.fin_head(&scope_addr),
);
} }
} }
@@ -589,6 +692,19 @@ impl<'bump> DiffMachine<'bump> {
self.attach_suspended_node_to_scope(new); 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 // Utilities for creating new diff instructions
// ============================================= // =============================================
@@ -627,7 +743,14 @@ impl<'bump> DiffMachine<'bump> {
.create_children(new, MountType::Replace { old: &old[0] }); .create_children(new, MountType::Replace { old: &old[0] });
} }
(_, [VNode::Anchor(_)]) => { (_, [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(); 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. /// If there is no offset, then this function returns None and the diffing is complete.
fn diff_keyed_ends( fn diff_keyed_ends(
&mut self, &mut self,
old: &'bump [VNode<'bump>], old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>],
) -> Option<(usize, usize)> { ) -> 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. // 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>]) { 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. 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!("old_key_to_old_index, {:#?}", old_key_to_old_index);
log::debug!("new_index_to_old_index, {:#?}", new_index_to_old_index); log::debug!("new_index_to_old_index, {:#?}", new_index_to_old_index);
log::debug!("shared_keys, {:#?}", shared_keys); 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; return;
} }
@@ -1032,14 +1163,16 @@ impl<'bump> DiffMachine<'bump> {
VNode::Element(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(),
VNode::Suspended(t) => break t.dom_id.get(), VNode::Suspended(t) => break t.dom_id.get(),
VNode::Anchor(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) => { VNode::Fragment(frag) => {
search_node = frag.children.last(); search_node = frag.children.last();
} }
VNode::Component(el) => { VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap(); let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(self.scopes.root_node(&scope_id));
search_node = Some(scope.root_node());
} }
} }
} }
@@ -1056,8 +1189,11 @@ impl<'bump> DiffMachine<'bump> {
} }
VNode::Component(el) => { VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap(); let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap(); search_node = Some(self.scopes.root_node(&scope_id));
search_node = Some(scope.root_node()); }
VNode::Linked(link) => {
let node = unsafe { std::mem::transmute(&*link.node) };
search_node = Some(node);
} }
VNode::Text(t) => break t.dom_id.get(), VNode::Text(t) => break t.dom_id.get(),
VNode::Element(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( fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) {
&mut self, match old {
old: &'bump [VNode<'bump>], VNode::Element(el) => {
new: &'bump VNode<'bump>, let id = old.try_mounted_id().expect(&format!("broke on {:?}", old));
) { self.mutations.replace_with(id, nodes_created as u32);
if let Some(first_old) = old.get(0) { self.remove_nodes(el.children, false);
self.remove_nodes(&old[1..], true); }
self.stack
.create_node(new, MountType::Replace { old: first_old }); VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
} else { let id = old.try_mounted_id().expect(&format!("broke on {:?}", old));
self.stack.create_node(new, MountType::Append {}); 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 { for node in nodes {
match node { match node {
VNode::Text(t) => { VNode::Text(t) => {
let id = t.dom_id.get().unwrap(); // this check exists because our null node will be removed but does not have an ID
self.vdom.collect_garbage(id); if let Some(id) = t.dom_id.get() {
self.scopes.collect_garbage(id);
if gen_muts { if gen_muts {
self.mutations.remove(id.as_u64()); self.mutations.remove(id.as_u64());
}
} }
} }
VNode::Suspended(s) => { VNode::Suspended(s) => {
let id = s.dom_id.get().unwrap(); let id = s.dom_id.get().unwrap();
self.vdom.collect_garbage(id); self.scopes.collect_garbage(id);
if gen_muts { if gen_muts {
self.mutations.remove(id.as_u64()); self.mutations.remove(id.as_u64());
@@ -1109,7 +1268,7 @@ impl<'bump> DiffMachine<'bump> {
} }
VNode::Anchor(a) => { VNode::Anchor(a) => {
let id = a.dom_id.get().unwrap(); let id = a.dom_id.get().unwrap();
self.vdom.collect_garbage(id); self.scopes.collect_garbage(id);
if gen_muts { if gen_muts {
self.mutations.remove(id.as_u64()); self.mutations.remove(id.as_u64());
@@ -1129,56 +1288,45 @@ impl<'bump> DiffMachine<'bump> {
self.remove_nodes(f.children, gen_muts); self.remove_nodes(f.children, gen_muts);
} }
VNode::Component(c) => { VNode::Linked(l) => {
let scope_id = c.associated_scope.get().unwrap(); let node = unsafe { std::mem::transmute(&*l.node) };
let scope = self.vdom.get_scope_mut(scope_id).unwrap(); self.remove_nodes(Some(node), gen_muts);
let root = scope.root_node(); }
self.remove_nodes(Some(root), gen_muts);
log::debug!("Destroying scope {:?}", scope_id); VNode::Component(c) => {
let mut s = self.vdom.try_remove(scope_id).unwrap(); self.destroy_vomponent(c, gen_muts);
s.hooks.clear_hooks();
} }
} }
} }
} }
/// Remove all the old nodes and replace them with newly created new nodes. fn destroy_vomponent(&mut self, vc: &VComponent, gen_muts: bool) {
/// let scope_id = vc.associated_scope.get().unwrap();
/// The new nodes *will* be created - don't create them yourself! let root = self.scopes.root_node(&scope_id);
fn replace_and_create_many_with_many( self.remove_nodes(Some(root), gen_muts);
&mut self, log::debug!("Destroying scope {:?}", scope_id);
old: &'bump [VNode<'bump>], self.scopes.try_remove(&scope_id).unwrap();
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 {});
}
} }
/// Adds a listener closure to a scope during diff. /// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &ScopeInner) { fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &Scope) {
let mut queue = scope.listeners.borrow_mut(); let long_listener = unsafe { std::mem::transmute(listener) };
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; scope.items.borrow_mut().listeners.push(long_listener)
queue.push(long_listener as *const _)
} }
fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) { fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) {
if let Some(scope) = self if let Some(scope) = self
.stack .stack
.current_scope() .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 // 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 scope
.suspended_nodes .items
.borrow_mut() .borrow_mut()
.insert(suspended.task_id, extended as *const _); .suspended_nodes
.insert(suspended.task_id, extended);
} }
} }
} }
-117
View File
@@ -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<DiffInstruction<'bump>>,
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<DiffInstruction<'bump>> {
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<ScopeId> {
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 });
}
}
-50
View File
@@ -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<FcSlot, Heuristic>,
}
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<T>(&mut self, fc: FC<T>, heuristic: Heuristic) {
let g = fc as FcSlot;
let e = self.heuristics.entry(g);
}
fn get_recommendation<T>(&mut self, fc: FC<T>) -> &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();
}
-66
View File
@@ -1,66 +0,0 @@
use std::{
any::Any,
cell::{Cell, RefCell, UnsafeCell},
};
type UnsafeInnerHookState = UnsafeCell<Box<dyn Any>>;
type HookCleanup = Box<dyn FnOnce(Box<dyn Any>)>;
/// 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<Vec<(UnsafeInnerHookState, HookCleanup)>>,
idx: Cell<usize>,
}
impl HookList {
pub(crate) fn next<T: 'static>(&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::<T>()
})
}
/// 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<T: 'static>(&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()));
}
}
-208
View File
@@ -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<Out>)
where
Out: 'static,
Fut: Future<Output = Out> + 'static,
Init: FnOnce() -> Fut + 'src,
{
struct TaskHook<T> {
handle: TaskHandle,
task_dump: Rc<RefCell<Option<T>>>,
value: Option<T>,
}
// 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<Output = Out> + '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<dyn Any>);
originator
},
)));
SuspenseHook { handle, value }
},
move |hook| match hook.value.borrow().as_ref() {
Some(value) => {
let out = value.downcast_ref::<Out>().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::<Out>()
// .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<RefCell<Option<Box<dyn Any>>>>,
}
pub struct SuspendedContext<'a> {
pub(crate) inner: Context<'a>,
}
impl<'src> SuspendedContext<'src> {
// // pub fn render(
// pub fn render(
// // pub fn render<F: FnOnce(NodeFactory<'src>) -> 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<Option<T>>);
impl<'a, T> Deref for NodeRef<'a, T> {
type Target = RefCell<Option<T>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
pub fn use_node_ref<T, P>(cx: Context) -> NodeRef<T> {
cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {})
}
+154 -73
View File
@@ -27,89 +27,107 @@ pub struct LazyNodes<'a, 'b> {
inner: StackNodeStorage<'a, 'b>, inner: StackNodeStorage<'a, 'b>,
} }
type StackHeapSize = [usize; 0]; type StackHeapSize = [usize; 16];
enum StackNodeStorage<'a, 'b> { enum StackNodeStorage<'a, 'b> {
Stack(LazyStack), Stack(LazyStack),
Heap(Box<dyn FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b>), Heap(Box<dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b>),
} }
impl<'a, 'b> LazyNodes<'a, 'b> { impl<'a, 'b> LazyNodes<'a, 'b> {
pub fn new<F>(val: F) -> Self pub fn new<F>(_val: F) -> Self
where where
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
{ {
unsafe { // 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 ptr: *const _ = &val as &dyn FnOnce(NodeFactory<'a>) -> VNode<'a>; let mut slot = Some(_val);
assert_eq!( let val = move |fac: Option<NodeFactory<'a>>| {
ptr as *const u8, &val as *const _ as *const u8, let inner = slot.take().unwrap();
"MISUSE: Closure returned different pointer" fac.map(inner)
); };
assert_eq!(
std::mem::size_of_val(&*ptr),
std::mem::size_of::<F>(),
"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...)"
);
// - Ensure that Self is aligned same as data requires unsafe { LazyNodes::new_inner(val) }
assert!( }
std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
"TODO: Enforce alignment >{} (requires {})",
std::mem::align_of::<Self>(),
std::mem::align_of::<F>()
);
let info = &words[1..]; unsafe fn new_inner<F>(val: F) -> Self
let data = words[0] as *mut (); where
let size = mem::size_of::<F>(); F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
{
let mut ptr: *const _ = &val as &dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>;
if info.len() * mem::size_of::<usize>() + size > mem::size_of::<StackHeapSize>() { assert_eq!(
log::debug!("lazy nodes was too large to fit into stack. falling back to heap"); 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::<F>(),
"MISUSE: Closure returned a subset pointer"
);
Self { let words = ptr_as_slice(&mut ptr);
inner: StackNodeStorage::Heap(Box::new(val)), 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::<F>() <= std::mem::align_of::<Self>(),
"TODO: Enforce alignment >{} (requires {})",
std::mem::align_of::<Self>(),
std::mem::align_of::<F>()
);
let info = &words[1..];
let data = words[0] as *mut ();
let size = mem::size_of::<F>();
let stored_size = info.len() * mem::size_of::<usize>() + size;
let max_size = mem::size_of::<StackHeapSize>();
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 std::mem::forget(val);
// - 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;
}
}
let src_ptr = data as *const u8; Self {
let dataptr = buf.as_mut()[..].as_mut_ptr() as *mut u8; inner: StackNodeStorage::Stack(LazyStack {
for i in 0..size { _align: [],
*dataptr.add(i) = *src_ptr.add(i); buf,
} dropped: false,
}),
std::mem::forget(val);
Self {
inner: StackNodeStorage::Stack(LazyStack { _align: [], buf }),
}
} }
} }
} }
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
match self.inner { match self.inner {
StackNodeStorage::Heap(lazy) => lazy(f), StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(),
StackNodeStorage::Stack(stack) => stack.call(f), StackNodeStorage::Stack(mut stack) => stack.call(f),
} }
} }
} }
@@ -117,34 +135,51 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
struct LazyStack { struct LazyStack {
_align: [u64; 0], _align: [u64; 0],
buf: StackHeapSize, buf: StackHeapSize,
dropped: bool,
} }
impl LazyStack { impl LazyStack {
unsafe fn create_boxed<'a>(&mut self) -> Box<dyn FnOnce(NodeFactory<'a>) -> VNode<'a>> { fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> {
let LazyStack { buf, .. } = self; let LazyStack { buf, .. } = self;
let data = buf.as_ref(); let data = buf.as_ref();
let info_size = mem::size_of::<*mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a>>() let info_size =
/ mem::size_of::<usize>() mem::size_of::<*mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>>()
- 1; / mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size; let info_ofs = data.len() - info_size;
let g: *mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a> = let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]); 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 clos = unsafe { &mut *g };
let boxed = unsafe { self.create_boxed() }; clos(Some(f)).unwrap()
boxed(f)
} }
} }
impl Drop for LazyStack { impl Drop for LazyStack {
fn drop(&mut self) { fn drop(&mut self) {
let boxed = unsafe { self.create_boxed() }; if !self.dropped {
mem::drop(boxed); let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size = mem::size_of::<
*mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>>,
>() / mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>> =
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() { fn it_works() {
let bump = bumpalo::Bump::new(); let bump = bumpalo::Bump::new();
simple_logger::init(); simple_logger::init().unwrap();
let factory = NodeFactory { bump: &bump }; let factory = NodeFactory { bump: &bump };
@@ -191,3 +226,49 @@ fn it_works() {
dbg!(g); 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::<Vec<_>>();
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));
}
+18 -48
View File
@@ -12,69 +12,39 @@ Navigating this crate:
Some utilities Some utilities
*/ */
pub mod bumpframe; pub(crate) mod component;
pub mod childiter; pub(crate) mod diff;
pub mod component; pub(crate) mod lazynodes;
pub mod context; pub(crate) mod mutations;
pub mod diff; pub(crate) mod nodes;
pub mod diff_stack; pub(crate) mod scope;
pub mod events; pub(crate) mod scopearena;
pub mod heuristics; pub(crate) mod virtual_dom;
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 innerlude { pub(crate) mod innerlude {
pub(crate) use crate::bumpframe::*;
pub(crate) use crate::childiter::*;
pub use crate::component::*; pub use crate::component::*;
pub use crate::context::*; pub use crate::diff::*;
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::lazynodes::*; pub use crate::lazynodes::*;
pub use crate::mutations::*; pub use crate::mutations::*;
pub use crate::nodes::*; pub use crate::nodes::*;
pub(crate) use crate::resources::*;
pub use crate::scheduler::*;
pub use crate::scope::*; pub use crate::scope::*;
pub use crate::tasks::*; pub use crate::scopearena::*;
pub use crate::test_dom::*;
pub use crate::threadsafe::*;
pub use crate::util::*;
pub use crate::virtual_dom::*; pub use crate::virtual_dom::*;
pub type Element<'a> = Option<VNode<'a>>; pub type Element = Option<NodeLink>;
pub type FC<P> = for<'a> fn(Scope<'a, P>) -> Element<'a>; pub type FC<P> = for<'a> fn(Context<'a>, &'a P) -> Element;
} }
pub use crate::innerlude::{ pub use crate::innerlude::{
Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType, Attribute, Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, IntoVNode,
Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, SuspendedContext, LazyNodes, Listener, MountType, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeId,
TaskHandle, TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, UserEvent, VAnchor, VElement, VFragment, VNode, VSuspended, VirtualDom, FC,
}; };
pub mod prelude { pub mod prelude {
pub use crate::component::{fc_to_builder, Fragment, Properties, Scope}; pub use crate::component::{fc_to_builder, Fragment, Properties};
pub use crate::context::Context; pub use crate::innerlude::Context;
pub use crate::hooks::*; pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, Scope, FC};
pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, ScopeChildren, FC};
pub use crate::nodes::VNode; pub use crate::nodes::VNode;
pub use crate::VirtualDom; pub use crate::VirtualDom;
} }
+16 -5
View File
@@ -3,21 +3,32 @@
//! This module contains an internal API to generate these instructions. //! This module contains an internal API to generate these instructions.
use crate::innerlude::*; use crate::innerlude::*;
use std::any::Any; use std::{any::Any, fmt::Debug};
#[derive(Debug)]
pub struct Mutations<'a> { pub struct Mutations<'a> {
pub edits: Vec<DomEdit<'a>>, pub edits: Vec<DomEdit<'a>>,
pub noderefs: Vec<NodeRefMutation<'a>>, pub noderefs: Vec<NodeRefMutation<'a>>,
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::*; use DomEdit::*;
impl<'a> Mutations<'a> { impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let edits = Vec::new(); Self {
let noderefs = Vec::new(); edits: Vec::new(),
Self { edits, noderefs } noderefs: Vec::new(),
effects: Vec::new(),
}
} }
// Navigation // Navigation
-119
View File
@@ -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<EventTrigger> {
// 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)
// }
+265 -149
View File
@@ -4,14 +4,12 @@
//! cheap and *very* fast to construct - building a full tree should be quick. //! cheap and *very* fast to construct - building a full tree should be quick.
use crate::{ use crate::{
innerlude::{ innerlude::{Context, Element, Properties, Scope, ScopeId},
empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner,
SuspendedContext,
},
lazynodes::LazyNodes, lazynodes::LazyNodes,
}; };
use bumpalo::{boxed::Box as BumpBox, Bump}; use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{ use std::{
any::Any,
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter}, fmt::{Arguments, Debug, Formatter},
}; };
@@ -19,8 +17,8 @@ use std::{
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. /// 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: /// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of:
///
/// - the [`rsx`] macro /// - the [`rsx`] macro
/// - the [`html`] macro
/// - the [`NodeFactory`] API /// - the [`NodeFactory`] API
pub enum VNode<'src> { pub enum VNode<'src> {
/// Text VNodes simply bump-allocated (or static) string slices /// Text VNodes simply bump-allocated (or static) string slices
@@ -28,7 +26,8 @@ pub enum VNode<'src> {
/// # Example /// # 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 { /// if let VNode::Text(vtext) = node {
/// assert_eq!(vtext.text, "hello"); /// assert_eq!(vtext.text, "hello");
@@ -43,7 +42,9 @@ pub enum VNode<'src> {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// let node = cx.render(rsx!{ /// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!{
/// div { /// div {
/// key: "a", /// key: "a",
/// onclick: |e| log::info!("clicked"), /// onclick: |e| log::info!("clicked"),
@@ -51,7 +52,8 @@ pub enum VNode<'src> {
/// style: { background_color: "red" } /// style: { background_color: "red" }
/// "hello" /// "hello"
/// } /// }
/// }).unwrap(); /// });
///
/// if let VNode::Element(velement) = node { /// if let VNode::Element(velement) = node {
/// assert_eq!(velement.tag_name, "div"); /// assert_eq!(velement.tag_name, "div");
/// assert_eq!(velement.namespace, None); /// assert_eq!(velement.namespace, None);
@@ -80,13 +82,13 @@ pub enum VNode<'src> {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// fn Example(cx: Context<()>) -> DomTree { /// fn Example(cx: Context, props: &()) -> Element {
/// todo!() /// todo!()
/// } /// }
/// ///
/// let node = cx.render(rsx!{ /// let mut vdom = VirtualDom::new();
/// Example {} ///
/// }).unwrap(); /// let node = vdom.render_vnode(rsx!( Example {} ));
/// ///
/// if let VNode::Component(vcomp) = node { /// if let VNode::Component(vcomp) = node {
/// assert_eq!(vcomp.user_fc, Example as *const ()); /// 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. /// 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 /// # Example
/// ///
/// ```rust /// ```rust
/// rsx!{ ///
/// } ///
/// ``` /// ```
Suspended(&'src VSuspended<'src>), Suspended(&'src VSuspended<'src>),
@@ -113,13 +113,32 @@ pub enum VNode<'src> {
/// # Example /// # Example
/// ///
/// ```rust /// ```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 { /// if let VNode::Fragment(frag) = node {
/// let root = &frag.children[0]; /// let root = &frag.children[0];
/// assert_eq!(root, VNode::Anchor); /// assert_eq!(root, VNode::Anchor);
/// } /// }
/// ``` /// ```
Anchor(&'src VAnchor), 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> { impl<'src> VNode<'src> {
@@ -129,9 +148,11 @@ impl<'src> VNode<'src> {
VNode::Element(el) => el.key, VNode::Element(el) => el.key,
VNode::Component(c) => c.key, VNode::Component(c) => c.key,
VNode::Fragment(f) => f.key, VNode::Fragment(f) => f.key,
VNode::Text(_t) => None, VNode::Text(_t) => None,
VNode::Suspended(_s) => None, VNode::Suspended(_s) => None,
VNode::Anchor(_f) => 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::Element(el) => el.dom_id.get(),
VNode::Anchor(el) => el.dom_id.get(), VNode::Anchor(el) => el.dom_id.get(),
VNode::Suspended(el) => el.dom_id.get(), VNode::Suspended(el) => el.dom_id.get(),
VNode::Linked(_) => None,
VNode::Fragment(_) => None, VNode::Fragment(_) => None,
VNode::Component(_) => 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. // Create an "owned" version of the vnode.
pub fn decouple(&self) -> VNode<'src> { pub fn decouple(&self) -> VNode<'src> {
match self { match self {
@@ -167,7 +198,11 @@ impl<'src> VNode<'src> {
VNode::Fragment(f) => VNode::Fragment(VFragment { VNode::Fragment(f) => VNode::Fragment(VFragment {
children: f.children, children: f.children,
key: f.key, 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> { fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self { match &self {
VNode::Element(el) => s VNode::Element(el) => s
.debug_struct("VElement") .debug_struct("VNode::VElement")
.field("name", &el.tag_name) .field("name", &el.tag_name)
.field("key", &el.key) .field("key", &el.key)
.finish(), .finish(),
VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text),
VNode::Anchor(_) => write!(s, "VAnchor"), VNode::Anchor(_) => write!(s, "VNode::VAnchor"),
VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children), VNode::Fragment(frag) => {
VNode::Suspended { .. } => write!(s, "VSuspended"), write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
VNode::Component(comp) => write!(s, "VComponent {{ fc: {:?}}}", comp.user_fc), }
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<Option<ElementId>> {
Cell::new(None)
}
/// A placeholder node only generated when Fragments don't have any children. /// A placeholder node only generated when Fragments don't have any children.
pub struct VAnchor { pub struct VAnchor {
pub dom_id: Cell<Option<ElementId>>, pub dom_id: Cell<Option<ElementId>>,
@@ -209,10 +269,7 @@ pub struct VText<'src> {
/// A list of VNodes with no single root. /// A list of VNodes with no single root.
pub struct VFragment<'src> { pub struct VFragment<'src> {
pub key: Option<&'src str>, pub key: Option<&'src str>,
pub children: &'src [VNode<'src>], pub children: &'src [VNode<'src>],
pub is_static: bool,
} }
/// An element like a "div" with children, listeners, and attributes. /// 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. /// IE "click" - whatever the renderer needs to attach the listener by name.
pub event: &'static str, pub event: &'static str,
#[allow(clippy::type_complexity)]
/// The actual callback that the user specified /// The actual callback that the user specified
pub(crate) callback: pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(Box<dyn Any + Send>) + 'bump>>>,
RefCell<Option<BumpBox<'bump, dyn FnMut(Box<dyn std::any::Any + Send>) + 'bump>>>,
} }
pub type VCompCaller<'src> = BumpBox<'src, dyn Fn(Context) -> Element + 'src>;
/// Virtual Components for custom user-defined components /// Virtual Components for custom user-defined components
/// Only supports the functional syntax /// Only supports the functional syntax
pub struct VComponent<'src> { pub struct VComponent<'src> {
@@ -320,21 +376,23 @@ pub struct VComponent<'src> {
pub associated_scope: Cell<Option<ScopeId>>, pub associated_scope: Cell<Option<ScopeId>>,
pub is_static: bool,
// Function pointer to the FC that was used to generate this component // Function pointer to the FC that was used to generate this component
pub user_fc: *const (), 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<Option<*const ()>>,
// 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<VCompCaller<'src>>,
pub(crate) caller: &'src dyn Fn(&'src Scope) -> Element,
pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>, pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>, pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
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> { pub struct VSuspended<'a> {
@@ -342,7 +400,41 @@ pub struct VSuspended<'a> {
pub dom_id: Cell<Option<ElementId>>, pub dom_id: Cell<Option<ElementId>>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub callback: RefCell<Option<BumpBox<'a, dyn FnMut(SuspendedContext<'a>) -> Element<'a>>>>, pub callback: RefCell<Option<BumpBox<'a, dyn FnMut() -> 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<usize>,
pub(crate) scope_id: Cell<Option<ScopeId>>,
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. /// This struct provides an ergonomic API to quickly build VNodes.
@@ -361,7 +453,7 @@ impl<'a> NodeFactory<'a> {
#[inline] #[inline]
pub fn bump(&self) -> &'a bumpalo::Bump { 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. /// 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), Some(static_str) => (static_str, true),
None => { None => {
use bumpalo::core_alloc::fmt::Write; 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.write_fmt(args).unwrap();
(str_buf.into_bump_str(), false) (str_buf.into_bump_str(), false)
} }
@@ -437,18 +529,18 @@ impl<'a> NodeFactory<'a> {
A: 'a + AsRef<[Attribute<'a>]>, A: 'a + AsRef<[Attribute<'a>]>,
V: 'a + AsRef<[VNode<'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 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 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 children = children.as_ref();
let key = key.map(|f| self.raw_text(f).0); let key = key.map(|f| self.raw_text(f).0);
VNode::Element(self.bump().alloc(VElement { VNode::Element(self.bump.alloc(VElement {
tag_name, tag_name,
key, key,
namespace, namespace,
@@ -479,19 +571,19 @@ impl<'a> NodeFactory<'a> {
pub fn component<P>( pub fn component<P>(
&self, &self,
component: fn(Scope<'a, P>) -> Element<'a>, component: fn(Context<'a>, &'a P) -> Element,
props: P, props: P,
key: Option<Arguments>, key: Option<Arguments>,
) -> VNode<'a> ) -> VNode<'a>
where where
P: Properties + 'a, P: Properties + 'a,
{ {
let bump = self.bump(); let bump = self.bump;
let props = bump.alloc(props); 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 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| { move |other: &VComponent| {
if user_fc == other.user_fc { if user_fc == other.user_fc {
// Safety // Safety
@@ -500,12 +592,10 @@ impl<'a> NodeFactory<'a> {
// - Non-static P are autoderived to memoize as false // - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes // - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe { 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) 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 // 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 // Implementing memoize is unsafe and done automatically with the props trait
props_memoized props_memoized
@@ -513,7 +603,7 @@ impl<'a> NodeFactory<'a> {
false false
} }
} }
})); });
let drop_props = { let drop_props = {
// create a closure to drop the 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(|| { let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe { move || unsafe {
log::debug!("dropping props!");
if !has_dropped { 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); let b = BumpBox::from_raw(real_other);
std::mem::drop(b); std::mem::drop(b);
@@ -541,36 +630,45 @@ impl<'a> NodeFactory<'a> {
let key = key.map(|f| self.raw_text(f).0); let key = key.map(|f| self.raw_text(f).0);
let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = let caller: &'a mut dyn Fn(&'a Scope) -> Element =
bump.alloc(move |scope: &ScopeInner| -> Element { bump.alloc(move |scope: &Scope| -> Element {
log::debug!("calling component renderr {:?}", scope.our_arena_idx); let props: &'_ P = unsafe { &*(bump_props as *const P) };
let props: &'_ P = unsafe { &*(raw_props as *const P) }; let res = component(scope, props);
res
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 can_memoize = P::IS_STATIC;
VNode::Component(bump.alloc(VComponent { VNode::Component(bump.alloc(VComponent {
user_fc, user_fc,
comparator, comparator: Some(comparator),
raw_props, bump_props,
caller, caller,
is_static: P::IS_STATIC,
key, key,
can_memoize: P::IS_STATIC, can_memoize,
drop_props, drop_props,
associated_scope: Cell::new(None), associated_scope: Cell::new(None),
hard_allocation: Cell::new(None),
})) }))
} }
pub fn listener(
self,
event: &'static str,
callback: BumpBox<'a, dyn FnMut(Box<dyn Any + Send>) + 'a>,
) -> Listener<'a> {
Listener {
mounted_node: Cell::new(None),
event,
callback: RefCell::new(Some(callback)),
}
}
pub fn fragment_from_iter( pub fn fragment_from_iter(
self, self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>, node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
) -> VNode<'a> { ) -> VNode<'a> {
let bump = self.bump(); let bump = self.bump;
let mut nodes = bumpalo::collections::Vec::new_in(bump); let mut nodes = bumpalo::collections::Vec::new_in(bump);
for node in node_iter { for node in node_iter {
@@ -607,14 +705,64 @@ impl<'a> NodeFactory<'a> {
VNode::Fragment(VFragment { VNode::Fragment(VFragment {
children, children,
key: None, key: None,
is_static: false,
}) })
} }
pub fn annotate_lazy<'z, 'b, F>(f: F) -> Option<LazyNodes<'z, 'b>> // this isn't quite feasible yet
where // I think we need some form of interior mutability or state on nodefactory that stores which subtree was created
F: FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b, pub fn create_children(
{ self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
) -> 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<LazyNodes<'z, 'b>> {
Some(LazyNodes::new(f)) Some(LazyNodes::new(f))
} }
} }
@@ -687,7 +835,6 @@ impl<'a> IntoVNode<'a> for Option<LazyNodes<'a, '_>> {
Some(lazy) => lazy.call(cx), Some(lazy) => lazy.call(cx),
None => VNode::Fragment(VFragment { None => VNode::Fragment(VFragment {
children: &[], children: &[],
is_static: false,
key: None, key: None,
}), }),
} }
@@ -712,79 +859,48 @@ impl IntoVNode<'_> for Arguments<'_> {
} }
} }
/// Access the children elements passed into the component // called cx.render from a helper function
/// impl IntoVNode<'_> for Option<NodeLink> {
/// This enables patterns where a component is passed children from its parent. fn into_vnode(self, _cx: NodeFactory) -> VNode {
/// match self {
/// ## Details Some(node) => VNode::Linked(node),
/// None => {
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions todo!()
/// 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<VNode<'a>>,
}
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<VNode<'a>>) -> Self {
Self { root }
}
}
impl IntoIterator for &ScopeChildren<'_> {
type Item = Self;
type IntoIter = std::iter::Once<Self>;
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<VNode>),
} }
} }
} }
// essentially passing elements through props
// just build a new element in place
impl IntoVNode<'_> for &Option<NodeLink> {
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,
})
}
}
-77
View File
@@ -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<UnsafeCell<Slab<ScopeInner>>>,
/*
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<UnsafeCell<Slab<*const VNode<'static>>>>,
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<ScopeInner> {
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
}
}
-609
View File
@@ -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<Cell<u64>>,
pub cur_subtree: Rc<Cell<u32>>,
pub sender: UnboundedSender<SchedulerMsg>,
pub schedule_any_immediate: Rc<dyn Fn(ScopeId)>,
pub submit_task: Rc<dyn Fn(FiberTask) -> TaskHandle>,
pub get_shared_context: GetSharedContext,
}
pub type GetSharedContext = Rc<dyn Fn(ScopeId, TypeId) -> Option<Rc<dyn Any>>>;
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<SchedulerMsg>,
// Garbage stored
pub pending_garbage: FxHashSet<ScopeId>,
// In-flight futures
pub async_tasks: FuturesUnordered<FiberTask>,
// // scheduler stuff
// pub current_priority: EventPriority,
pub ui_events: VecDeque<UserEvent>,
pub pending_immediates: VecDeque<ScopeId>,
pub pending_tasks: VecDeque<UserEvent>,
pub batched_events: VecDeque<UserEvent>,
pub garbage_scopes: HashSet<ScopeId>,
pub dirty_scopes: IndexSet<ScopeId>,
pub saved_state: Option<SavedDiffWork<'static>>,
pub in_progress: bool,
}
impl Scheduler {
pub(crate) fn new(
sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>,
) -> 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<Mutations>,
) -> 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<Mutations<'a>> {
/*
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::<Mutations<'static>>::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<Mutations<'a>> {
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()
}
}
}
+454 -214
View File
@@ -1,14 +1,38 @@
use crate::innerlude::*; use crate::innerlude::*;
use futures_channel::mpsc::UnboundedSender;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::HashMap, collections::HashMap,
future::Future, future::Future,
pin::Pin,
rc::Rc, 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`. /// Every component in Dioxus is represented by a `Scope`.
/// ///
/// Scopes contain the state for hooks, the component's props, and other lifecycle information. /// 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 /// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever
/// use case they might have. /// use case they might have.
pub struct ScopeInner { pub struct Scope {
// Book-keeping about our spot in the arena // safety:
pub(crate) parent_idx: Option<ScopeId>, //
// 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) our_arena_idx: ScopeId,
pub(crate) height: u32, pub(crate) height: u32,
pub(crate) subtree: Cell<u32>, pub(crate) subtree: Cell<u32>,
pub(crate) is_subtree_root: Cell<bool>, pub(crate) is_subtree_root: Cell<bool>,
// Nodes pub(crate) generation: Cell<u32>,
pub(crate) frames: ActiveFrame,
pub(crate) caller: *const dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, // The double-buffering situation that we will use
pub(crate) frames: [BumpFrame; 2],
pub(crate) caller: *const dyn Fn(&Scope) -> Element,
/* /*
we care about: we care about:
@@ -36,9 +71,7 @@ pub struct ScopeInner {
- borrowed props (and how to drop them when the parent is dropped) - 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) - suspended nodes (and how to call their callback when their associated tasks are complete)
*/ */
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>, pub(crate) items: RefCell<SelfReferentialItems<'static>>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) suspended_nodes: RefCell<FxHashMap<u64, *const VSuspended<'static>>>,
// State // State
pub(crate) hooks: HookList, pub(crate) hooks: HookList,
@@ -46,34 +79,27 @@ pub struct ScopeInner {
// todo: move this into a centralized place - is more memory efficient // todo: move this into a centralized place - is more memory efficient
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>, pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
// whenever set_state is called, we fire off a message to the scheduler pub(crate) sender: UnboundedSender<SchedulerMsg>,
// this closure _is_ the method called by schedule_update that marks this component as dirty
pub(crate) memoized_updater: Rc<dyn Fn()>,
pub(crate) shared: EventChannel,
} }
/// Public interface for Scopes. pub struct SelfReferentialItems<'a> {
impl ScopeInner { pub(crate) listeners: Vec<&'a Listener<'a>>,
/// Get the root VNode for this Scope. pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
/// pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended<'a>>,
/// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment. pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
/// pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
/// # 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()
}
/// 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. /// 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 /// 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 /// # Example
/// ///
/// ```rust /// ```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(); /// dom.rebuild();
/// ///
/// let base = dom.base_scope(); /// let base = dom.base_scope();
@@ -94,16 +120,6 @@ impl ScopeInner {
self.subtree.get() self.subtree.get()
} }
pub(crate) fn new_subtree(&self) -> Option<u32> {
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. /// 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. /// 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 /// # Example
/// ///
/// ```rust /// ```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(); /// dom.rebuild();
/// ///
/// let base = dom.base_scope(); /// let base = dom.base_scope();
@@ -131,7 +147,7 @@ impl ScopeInner {
/// # Example /// # Example
/// ///
/// ```rust /// ```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(); /// dom.rebuild();
/// ///
/// let base = dom.base_scope(); /// let base = dom.base_scope();
@@ -139,7 +155,10 @@ impl ScopeInner {
/// assert_eq!(base.parent(), None); /// assert_eq!(base.parent(), None);
/// ``` /// ```
pub fn parent(&self) -> Option<ScopeId> { pub fn parent(&self) -> Option<ScopeId> {
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. /// Get the ID of this Scope within this Dioxus VirtualDOM.
@@ -149,7 +168,7 @@ impl ScopeInner {
/// # Example /// # Example
/// ///
/// ```rust /// ```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(); /// dom.rebuild();
/// let base = dom.base_scope(); /// let base = dom.base_scope();
/// ///
@@ -158,116 +177,290 @@ impl ScopeInner {
pub fn scope_id(&self) -> ScopeId { pub fn scope_id(&self) -> ScopeId {
self.our_arena_idx self.our_arena_idx
} }
}
// The type of closure that wraps calling components /// Create a subscription that schedules a future render for the reference component
/// 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 /// ## Notice: you should prefer using prepare_update and get_scope_id
pub type FiberTask = Pin<Box<dyn Future<Output = ScopeId>>>; pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
// pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
let chan = self.sender.clone();
let id = self.scope_id();
Rc::new(move || {
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
/// Private interface for Scopes. /// Schedule an update for any component given its ScopeId.
impl ScopeInner { ///
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play) /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method.
// 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 method should be used when you want to schedule an update for a component
// This should never happen, but is a good check to keep around pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
// let chan = self.sender.clone();
// Scopes cannot be made anywhere else except for this file Rc::new(move |id| {
// Therefore, their lifetimes are connected exclusively to the virtual dom let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
pub(crate) fn new( })
caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, }
our_arena_idx: ScopeId,
parent_idx: Option<ScopeId>,
height: u32,
subtree: u32,
shared: EventChannel,
) -> Self {
let schedule_any_update = shared.schedule_any_immediate.clone();
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 /// Get the [`ScopeId`] of a mounted component.
let caller = unsafe { std::mem::transmute(caller) }; ///
/// `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 { /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
memoized_updater, ///
shared, /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
caller, ///
parent_idx, /// ## Example
our_arena_idx, ///
height, /// ```ignore
subtree: Cell::new(subtree), /// fn Component(cx: Scope, props: &Props) -> Element {
is_subtree_root: Cell::new(false), /// // 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<LazyNodes<'src, '_>>) -> Option<NodeLink> {
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(), let node_ptr = node as *mut _;
hooks: Default::default(), let node_ptr = unsafe { std::mem::transmute(node_ptr) };
suspended_nodes: Default::default(),
shared_contexts: Default::default(), let link = NodeLink {
listeners: Default::default(), scope_id: Cell::new(Some(self.our_arena_idx)),
borrowed_props: Default::default(), 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<Output = ()> + 'src) -> usize {
// allocate the future
let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut);
// wrap it in a type that will actually drop the contents
let boxed_fut: BumpBox<dyn Future<Output = ()>> = 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::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" })
/// }
/// ```
pub fn provide_state<T: 'static>(&self, value: T) {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(value))
.map(|f| f.downcast::<T>().ok())
.flatten();
}
/// Try to retrieve a SharedState with type T from the any parent Scope.
pub fn consume_state<T: 'static>(&self) -> Option<Rc<T>> {
if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
Some(shared.clone().downcast::<T>().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::<T>()) {
return Some(shared.clone().downcast::<T>().unwrap());
}
search_parent = parent.parent_scope;
}
None
} }
} }
pub(crate) fn update_scope_dependencies( /// Create a new subtree with this scope as the root of the subtree.
&mut self, ///
caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, /// 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.
log::debug!("Updating scope dependencies {:?}", self.our_arena_idx); ///
let caller = caller as *const _; /// This method
self.caller = unsafe { std::mem::transmute(caller) }; ///
/// # Example
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// todo!();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn create_subtree(&self) -> Option<u32> {
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 /// Get the subtree ID that this scope belongs to.
/// 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 /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove /// the mutations to the correct window/portal/subtree.
/// 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 /// # Example
/// 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 /// ```rust
/// be dropped. /// fn App(cx: Context, props: &()) -> Element {
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) { /// let id = cx.get_current_subtree();
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we /// rsx!(cx, div { "Subtree {id}"})
// run the hooks (which hold an &mut Reference) /// };
// right now, we don't drop /// ```
self.borrowed_props pub fn get_current_subtree(&self) -> u32 {
.get_mut() self.subtree()
.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");
if let Some(scope) = pool.get_scope_mut(scope_id) { /// Store a value between renders
scope.ensure_drop_safety(pool); ///
/// 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<T: 'static>(initial_value: impl FnOnce() -> T) -> &RefCell<T> {
/// 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(); const HOOK_ERR_MSG: &str = r###"
drop_props(); 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. You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
self.listeners Functions prefixed with "use" should never be called conditionally.
.get_mut() "###;
.drain(..)
.map(|li| unsafe { &*li }) runner(self.hooks.next::<State>().expect(HOOK_ERR_MSG))
.for_each(|listener| drop(listener.callback.borrow_mut().take())); }
}
// 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 /// A safe wrapper around calling listeners
pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { pub(crate) fn call_listener(&self, event: UserEvent, element: ElementId) {
let listners = self.listeners.borrow_mut(); let listners = &mut self.items.borrow_mut().listeners;
let raw_listener = listners.iter().find(|lis| { let listener = listners.iter().find(|lis| {
let search = unsafe { &***lis }; let search = lis;
if search.event == event.name { if search.event == event.name {
let search_id = search.mounted_node.get(); let search_id = search.mounted_node.get();
search_id.map(|f| f == element).unwrap_or(false) search_id.map(|f| f == element).unwrap_or(false)
@@ -276,8 +469,7 @@ impl ScopeInner {
} }
}); });
if let Some(raw_listener) = raw_listener { if let Some(listener) = listener {
let listener = unsafe { &**raw_listener };
let mut cb = listener.callback.borrow_mut(); let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() { if let Some(cb) = cb.as_mut() {
(cb)(event.event); (cb)(event.event);
@@ -287,89 +479,137 @@ impl ScopeInner {
} }
} }
/* // General strategy here is to load up the appropriate suspended task and then run it.
General strategy here is to load up the appropriate suspended task and then run it. // Suspended nodes cannot be called repeatedly.
Suspended nodes cannot be called repeatedly.
*/
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { 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) { if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended }; let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let mut boxed = sus.callback.borrow_mut().take().unwrap();
let cx: SuspendedContext<'a> = SuspendedContext { let new_node: Element = boxed();
inner: Context { scope: self },
};
let mut cb = sus.callback.borrow_mut().take().unwrap();
let new_node: Element<'a> = (cb)(cx);
} }
} }
// run the list of effects // run the list of effects
pub(crate) fn run_effects(&mut self, pool: &ResourcePool) { pub(crate) fn run_effects(&mut self) {
todo!() for mut effect in self.items.get_mut().pending_effects.drain(..) {
// let mut effects = self.frames.effects.borrow_mut(); effect();
// let mut effects = effects.drain(..).collect::<Vec<_>>();
// 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 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<Vec<*const VNode<'static>>>,
}
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<SmallVec<[*mut dyn Any; 5]>>,
idx: Cell<usize>,
}
impl HookList {
pub fn new(capacity: usize) -> Self {
Self {
arena: Bump::with_capacity(capacity),
..Default::default()
}
}
pub(crate) fn next<T: 'static>(&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::<T>()
})
}
/// 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<T: 'static>(&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::<Scope>());
} }
+361
View File
@@ -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<usize>,
scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>,
pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
free_scopes: RefCell<Vec<*mut Scope>>,
nodes: RefCell<Slab<*const VNode<'static>>>,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
}
impl ScopeArena {
pub fn new(sender: UnboundedSender<SchedulerMsg>) -> 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)
}
}
-39
View File
@@ -1,39 +0,0 @@
use crate::innerlude::*;
use futures_channel::mpsc::UnboundedSender;
pub struct TaskHandle {
pub(crate) sender: UnboundedSender<SchedulerMsg>,
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()
}
}
-82
View File
@@ -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::<SchedulerMsg>();
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<LazyNodes<'a, '_>>) -> VNode<'a> {
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
pub fn render<'a>(&'a self, lazy_nodes: Option<LazyNodes<'a, '_>>) -> &'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<LazyNodes<'a, '_>>) -> 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<LazyNodes<'a, '_>>,
right: Option<LazyNodes<'a, '_>>,
) -> (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()
}
}
-73
View File
@@ -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<Mutex<VirtualDom>>,
}
impl ThreadsafeVirtualDom {
pub fn new(inner: VirtualDom) -> Self {
let inner = Arc::new(Mutex::new(inner));
Self { inner }
}
pub fn lock(&self) -> Option<VirtualDomGuard> {
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
}
}
-91
View File
@@ -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<Option<ElementId>> {
Cell::new(None)
}
pub fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
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<Self::Output> {
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
}
}
+515 -218
View File
@@ -1,72 +1,127 @@
//! # VirtualDOM Implementation for Rust //! # VirtualDOM Implementation for Rust
//! //!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM 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 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 /// ## Guide
/// to a particular crate's wrapper over the [`VirtualDom`] API. ///
/// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`].
/// ///
/// Example
/// ```rust /// ```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!{ /// cx.render(rsx!{
/// div { /// div { "Hello World" }
/// "Hello World"
/// }
/// }) /// })
/// } /// }
/// ///
/// async fn main() { /// async fn main() {
/// let mut dom = VirtualDom::new(App); /// let mut dom = VirtualDom::new(App);
///
/// let mut inital_edits = dom.rebuild(); /// let mut inital_edits = dom.rebuild();
/// initialize_screen(inital_edits); /// apply_edits(inital_edits);
/// ///
/// loop { /// loop {
/// let next_frame = TimeoutFuture::new(Duration::from_millis(16)); /// dom.wait_for_work().await;
/// let edits = dom.run_with_deadline(next_frame).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); /// apply_edits(edits);
/// render_frame();
/// } /// }
/// } /// }
/// ``` /// ```
pub struct VirtualDom { pub struct VirtualDom {
scheduler: Scheduler,
base_scope: ScopeId, base_scope: ScopeId,
root_fc: Box<dyn Any>, _root_caller: *mut dyn Fn(&Scope) -> Element,
root_props: Rc<dyn Any>, scopes: Box<ScopeArena>,
// we need to keep the allocation around, but we don't necessarily use it receiver: UnboundedReceiver<SchedulerMsg>,
_root_caller: Rc<dyn Any>,
sender: UnboundedSender<SchedulerMsg>,
pending_futures: FxHashSet<ScopeId>,
pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>,
} }
// Methods to create the VirtualDom
impl VirtualDom { impl VirtualDom {
/// Create a new VirtualDOM with a component that does not have special props. /// Create a new VirtualDOM with a component that does not have special props.
/// ///
@@ -80,7 +135,7 @@ impl VirtualDom {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// fn Example(cx: Context<()>) -> DomTree { /// fn Example(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div { "hello world" } )) /// cx.render(rsx!( div { "hello world" } ))
/// } /// }
/// ///
@@ -109,7 +164,7 @@ impl VirtualDom {
/// name: &'static str /// name: &'static str
/// } /// }
/// ///
/// fn Example(cx: Context<SomeProps>) -> DomTree { /// fn Example(cx: Context, props: &SomeProps) -> Element {
/// cx.render(rsx!{ div{ "hello {cx.name}" } }) /// 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 mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild(); /// let mutations = dom.rebuild();
/// ``` /// ```
pub fn new_with_props<P: 'static + Send>(root: FC<P>, root_props: P) -> Self { pub fn new_with_props<P: 'static>(root: FC<P>, root_props: P) -> Self {
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>(); let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
Self::new_with_props_and_scheduler(root, root_props, sender, receiver) 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 /// 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. /// VirtualDom to be created just to retrieve its channel receiver.
@@ -137,38 +192,26 @@ impl VirtualDom {
sender: UnboundedSender<SchedulerMsg>, sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>, receiver: UnboundedReceiver<SchedulerMsg>,
) -> Self { ) -> Self {
let root_fc = Box::new(root); let scopes = ScopeArena::new(sender.clone());
let root_props: Rc<dyn Any> = 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 pending_messages = VecDeque::new();
let mut dirty_scopes = IndexSet::new();
let root_caller: Rc<dyn Fn(&ScopeInner) -> Element> = Rc::new(move |scope: &ScopeInner| { dirty_scopes.insert(base_scope);
let props = props.downcast_ref::<P>().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(),
)
});
Self { Self {
_root_caller: Rc::new(root_caller), scopes: Box::new(scopes),
root_fc,
base_scope, base_scope,
scheduler, receiver,
root_props, // 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 /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
/// directly. /// 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`] /// 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<AppProps> = |(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<P>(&mut self, root_props: P) -> Option<Mutations>
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<dyn Any> = Rc::new(root_props);
if let Some(props_ptr) = root_props.downcast_ref::<P>().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::<FC<P>>().unwrap();
let root_caller: Box<dyn Fn(&ScopeInner) -> 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 /// # 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 { pub fn get_scope<'a>(&'a self, id: &ScopeId) -> Option<&'a Scope> {
self.scheduler.rebuild(self.base_scope) self.scopes.get_scope(id)
} }
/// Compute a manual diff of the VirtualDOM between states. /// Get an [`UnboundedSender`] handle to the channel used by the scheduler.
///
/// 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 /// # Example
///
/// ```rust /// ```rust
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
/// }
/// ///
/// static App: FC<AppProps> = |(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 { pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
self.scheduler.hard_diff(self.base_scope) 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 /// # Example
/// return "None" ///
pub fn run_immediate(&mut self) -> Option<Vec<Mutations>> { /// ```rust
if self.scheduler.has_any_work() { ///
Some(self.scheduler.work_sync()) ///
/// ```
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 { } else {
None struct PollTasks<'a> {
pending_futures: &'a FxHashSet<ScopeId>,
scopes: &'a ScopeArena,
}
impl<'a> Future for PollTasks<'a> {
type Output = ();
fn poll(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Self::Output> {
let mut all_pending = true;
// 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 /// # Example
/// ///
/// ```no_run /// ```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); /// let mut dom = VirtualDom::new(App);
///
/// loop { /// 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; /// let mutations = dom.run_with_deadline(deadline).await;
///
/// apply_mutations(mutations); /// apply_mutations(mutations);
/// } /// }
/// ``` /// ```
@@ -350,48 +383,312 @@ impl VirtualDom {
/// applied the edits. /// applied the edits.
/// ///
/// Mutations are the only link between the RealDOM and the VirtualDOM. /// Mutations are the only link between the RealDOM and the VirtualDOM.
pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec<Mutations<'_>> { ///
self.scheduler.work_with_deadline(deadline) pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> {
} let mut committed_mutations = vec![];
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> { while self.has_any_work() {
self.scheduler.pool.channel.sender.clone() while let Ok(Some(msg)) = self.receiver.try_next() {
} self.pending_messages.push_front(msg);
/// 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, _)) => {
//
} }
// wait for an external event while let Some(msg) = self.pending_messages.pop_back() {
Either::Right((msg, _)) => match msg.unwrap() { match msg {
SchedulerMsg::Task(t) => { SchedulerMsg::Immediate(id) => {
self.scheduler.handle_task(t); 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<AppProps> = |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<Mutations<'a>> {
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<LazyNodes<'a, '_>>) -> &'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<LazyNodes<'a, '_>>) -> 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<LazyNodes<'a, '_>>,
right: Option<LazyNodes<'a, '_>>,
) -> (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<ElementId>,
/// 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<dyn Any + Send>,
}
/// 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,
}
+4 -4
View File
@@ -10,8 +10,8 @@ fn test_borrowed_state() {
let _ = VirtualDom::new(Parent); let _ = VirtualDom::new(Parent);
} }
fn Parent((cx, _): Scope<()>) -> Element { fn Parent(cx: Context, props: &()) -> Element {
let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {}); let value = cx.use_hook(|_| String::new(), |f| &*f);
cx.render(rsx! { cx.render(rsx! {
div { div {
@@ -28,7 +28,7 @@ struct ChildProps<'a> {
name: &'a str, name: &'a str,
} }
fn Child<'a>((cx, props): Scope<'a, ChildProps>) -> Element<'a> { fn Child(cx: Context, props: &ChildProps) -> Element {
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "it's nested" } h1 { "it's nested" }
@@ -42,7 +42,7 @@ struct Grandchild<'a> {
name: &'a str, name: &'a str,
} }
fn Child2<'a>((cx, props): Scope<'a, Grandchild>) -> Element<'a> { fn Child2(cx: Context, props: &Grandchild) -> Element {
cx.render(rsx! { cx.render(rsx! {
div { "Hello {props.name}!" } div { "Hello {props.name}!" }
}) })
+14 -31
View File
@@ -21,7 +21,7 @@ fn new_dom<P: 'static + Send>(app: FC<P>, props: P) -> VirtualDom {
#[test] #[test]
fn test_original_diff() { fn test_original_diff() {
static APP: FC<()> = |(cx, props)| { static APP: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
div { div {
@@ -57,17 +57,17 @@ fn test_original_diff() {
#[test] #[test]
fn create() { fn create() {
static APP: FC<()> = |(cx, props)| { static APP: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div { div {
div { div {
"Hello, world!" "Hello, world!"
div { div {
div { div {
// Fragment { Fragment {
// "hello" "hello"
// "world" "world"
// } }
} }
} }
} }
@@ -120,7 +120,7 @@ fn create() {
#[test] #[test]
fn create_list() { fn create_list() {
static APP: FC<()> = |(cx, props)| { static APP: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
{(0..3).map(|f| rsx!{ div { {(0..3).map(|f| rsx!{ div {
"hello" "hello"
@@ -169,7 +169,7 @@ fn create_list() {
#[test] #[test]
fn create_simple() { fn create_simple() {
static APP: FC<()> = |(cx, props)| { static APP: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
div {} div {}
div {} div {}
@@ -207,7 +207,7 @@ fn create_simple() {
} }
#[test] #[test]
fn create_components() { fn create_components() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
Child { "abc1" } Child { "abc1" }
Child { "abc2" } Child { "abc2" }
@@ -215,12 +215,12 @@ fn create_components() {
}) })
}; };
#[derive(Props)] #[derive(Props, PartialEq)]
struct ChildProps<'a> { struct ChildProps {
children: ScopeChildren<'a>, children: Element,
} }
fn Child<'a>((cx, props): Scope<'a, ChildProps<'a>>) -> Element { fn Child(cx: Context, props: &ChildProps) -> Element {
cx.render(rsx! { cx.render(rsx! {
h1 {} h1 {}
div { {&props.children} } div { {&props.children} }
@@ -273,7 +273,7 @@ fn create_components() {
} }
#[test] #[test]
fn anchors() { fn anchors() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
{true.then(|| rsx!{ div { "hello" } })} {true.then(|| rsx!{ div { "hello" } })}
{false.then(|| rsx!{ div { "goodbye" } })} {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 },]
);
}
+28 -47
View File
@@ -5,17 +5,17 @@
//! //!
//! It does not validated that component lifecycles work properly. This is done in another test file. //! 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 as dioxus;
use dioxus_core_macro::*; use dioxus_core_macro::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
mod test_logging; mod test_logging;
fn new_dom() -> TestDom { fn new_dom() -> VirtualDom {
const IS_LOGGING_ENABLED: bool = false; const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED); test_logging::set_up_logging(IS_LOGGING_ENABLED);
TestDom::new() VirtualDom::new(|cx, props| todo!())
} }
use DomEdit::*; use DomEdit::*;
@@ -24,7 +24,7 @@ use DomEdit::*;
#[test] #[test]
fn html_and_rsx_generate_the_same_output() { fn html_and_rsx_generate_the_same_output() {
let dom = new_dom(); let dom = new_dom();
let (create, change) = dom.lazy_diff( let (create, change) = dom.diff_lazynodes(
rsx! ( div { "Hello world" } ), rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ), rsx! ( div { "Goodbye world" } ),
); );
@@ -58,7 +58,7 @@ fn html_and_rsx_generate_the_same_output() {
fn fragments_create_properly() { fn fragments_create_properly() {
let dom = new_dom(); let dom = new_dom();
let create = dom.create(rsx! { let create = dom.create_vnodes(rsx! {
div { "Hello a" } div { "Hello a" }
div { "Hello b" } div { "Hello b" }
div { "Hello c" } div { "Hello c" }
@@ -107,7 +107,7 @@ fn empty_fragments_create_anchors() {
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..1).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!( assert_eq!(
create.edits, create.edits,
@@ -133,7 +133,7 @@ fn empty_fragments_create_many_anchors() {
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) }); let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..5).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!( assert_eq!(
create.edits, create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] [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!( assert_eq!(
create.edits, create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }] [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
@@ -231,7 +231,7 @@ fn many_items_become_fragment() {
}); });
let right = rsx!({ (0..0).map(|_| rsx! { div {} }) }); 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!( assert_eq!(
create.edits, 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!( assert_eq!(
change.edits, change.edits,
[ [
Remove { root: 2 }, Remove { root: 2 },
CreatePlaceholder { root: 4 }, CreatePlaceholder { root: 3 },
ReplaceWith { root: 0, m: 1 }, 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()); assert!(change.edits.is_empty());
} }
@@ -302,7 +302,7 @@ fn two_fragments_with_differrent_elements_are_differet() {
p {} p {}
); );
let (_create, changes) = dom.lazy_diff(left, right); let (_create, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", &changes); log::debug!("{:#?}", &changes);
assert_eq!( assert_eq!(
changes.edits, changes.edits,
@@ -335,7 +335,7 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
p {} p {}
); );
let (create, change) = dom.lazy_diff(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
create.edits, create.edits,
[ [
@@ -391,7 +391,7 @@ fn two_fragments_with_same_elements_are_differet() {
p {} p {}
); );
let (create, change) = dom.lazy_diff(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
create.edits, create.edits,
[ [
@@ -433,7 +433,7 @@ fn keyed_diffing_order() {
let dom = new_dom(); let dom = new_dom();
let left = rsx!( let left = rsx!(
// {(0..5).map(|f| {rsx! { div { key: "{f}" }}})} {(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
p {"e"} p {"e"}
); );
let right = rsx!( let right = rsx!(
@@ -441,7 +441,7 @@ fn keyed_diffing_order() {
p {"e"} p {"e"}
); );
let (create, change) = dom.lazy_diff(left, right); let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!( assert_eq!(
change.edits, change.edits,
[Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },] [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); log::debug!("{:?}", &changes);
assert_eq!( assert_eq!(
changes.edits, 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!( assert_eq!(
change.edits, 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!( assert_eq!(
change.edits, 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!( assert_eq!(
change.edits, 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!( assert_eq!(
change.edits, 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!( assert_eq!(
change.edits, change.edits,
[PushRoot { root: 4 }, InsertBefore { n: 1, root: 3 }] [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!( assert_eq!(
change.edits, 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); log::debug!("{:?}", change);
assert_eq!( assert_eq!(
change.edits, change.edits,
@@ -696,7 +696,7 @@ fn keyed_diffing_additions_and_moves_in_middle() {
}); });
// LIS: 4, 5, 6 // LIS: 4, 5, 6
let (_, change) = dom.lazy_diff(left, right); let (_, change) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", change); log::debug!("{:#?}", change);
assert_eq!( assert_eq!(
change.edits, change.edits,
@@ -745,7 +745,7 @@ fn controlled_keyed_diffing_out_of_order() {
}); });
// LIS: 5, 6 // LIS: 5, 6
let (_, changes) = dom.lazy_diff(left, right); let (_, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", &changes); log::debug!("{:#?}", &changes);
assert_eq!( assert_eq!(
changes.edits, 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); log::debug!("{:#?}", &changes);
assert_eq!( assert_eq!(
changes.edits, 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 }]
// );
}
-40
View File
@@ -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);
}
+274 -2
View File
@@ -3,7 +3,11 @@
//! Tests for the lifecycle of components. //! Tests for the lifecycle of components.
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core::DomEdit::*;
use dioxus_core::ScopeId;
use dioxus_core_macro::*; use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -20,7 +24,7 @@ fn manual_diffing() {
value: Shared<&'static str>, value: Shared<&'static str>,
} }
static App: FC<AppProps> = |(cx, props)| { static App: FC<AppProps> = |cx, props| {
let val = props.value.lock().unwrap(); let val = props.value.lock().unwrap();
cx.render(rsx! { div { "{val}" } }) cx.render(rsx! { div { "{val}" } })
}; };
@@ -37,7 +41,275 @@ fn manual_diffing() {
*value.lock().unwrap() = "goodbye"; *value.lock().unwrap() = "goodbye";
let edits = dom.diff(); let edits = dom.rebuild();
log::debug!("edits: {:?}", edits); 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);
}
+3 -3
View File
@@ -1,6 +1,6 @@
#![allow(unused, non_upper_case_globals)] #![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 as dioxus;
use dioxus_core_macro::*; use dioxus_core_macro::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
@@ -13,12 +13,12 @@ mod test_logging;
fn shared_state_test() { fn shared_state_test() {
struct MySharedState(&'static str); struct MySharedState(&'static str);
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.provide_state(MySharedState("world!")); cx.provide_state(MySharedState("world!"));
cx.render(rsx!(Child {})) cx.render(rsx!(Child {}))
}; };
static Child: FC<()> = |(cx, props)| { static Child: FC<()> = |cx, props| {
let shared = cx.consume_state::<MySharedState>()?; let shared = cx.consume_state::<MySharedState>()?;
cx.render(rsx!("Hello, {shared.0}")) cx.render(rsx!("Hello, {shared.0}"))
}; };
+1
View File
@@ -0,0 +1 @@
fn main() {}
+15 -14
View File
@@ -17,7 +17,7 @@ use dioxus_html as dioxus_elements;
#[test] #[test]
fn app_runs() { fn app_runs() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
// //
cx.render(rsx!( div{"hello"} )) cx.render(rsx!( div{"hello"} ))
}; };
@@ -28,7 +28,7 @@ fn app_runs() {
#[test] #[test]
fn fragments_work() { fn fragments_work() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
div{"hello"} div{"hello"}
div{"goodbye"} div{"goodbye"}
@@ -42,7 +42,7 @@ fn fragments_work() {
#[test] #[test]
fn lists_work() { fn lists_work() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
h1 {"hello"} h1 {"hello"}
{(0..6).map(|f| rsx!(span{ "{f}" }))} {(0..6).map(|f| rsx!(span{ "{f}" }))}
@@ -55,7 +55,7 @@ fn lists_work() {
#[test] #[test]
fn conditional_rendering() { fn conditional_rendering() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
h1 {"hello"} h1 {"hello"}
{true.then(|| rsx!(span{ "a" }))} {true.then(|| rsx!(span{ "a" }))}
@@ -72,13 +72,13 @@ fn conditional_rendering() {
#[test] #[test]
fn child_components() { fn child_components() {
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
{true.then(|| rsx!(Child { }))} {true.then(|| rsx!(Child { }))}
{false.then(|| rsx!(Child { }))} {false.then(|| rsx!(Child { }))}
)) ))
}; };
static Child: FC<()> = |(cx, props)| { static Child: FC<()> = |cx, props| {
cx.render(rsx!( cx.render(rsx!(
h1 {"hello"} h1 {"hello"}
h1 {"goodbye"} h1 {"goodbye"}
@@ -91,13 +91,14 @@ fn child_components() {
#[test] #[test]
fn suspended_works() { fn suspended_works() {
static App: FC<()> = |(cx, props)| { todo!()
let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!()); // static App: FC<()> = |cx, props| {
// let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"}); // let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!());
cx.render(rsx!("hello" { title })) // // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"});
}; // cx.render(rsx!("hello" { title }))
// };
let mut vdom = VirtualDom::new(App); // let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild(); // let edits = vdom.rebuild();
dbg!(edits); // dbg!(edits);
} }
+1 -1
View File
@@ -24,6 +24,7 @@ tokio = { version = "1.12.0", features = [
"rt", "rt",
], optional = true, default-features = false } ], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro" } dioxus-core-macro = { path = "../core-macro" }
dioxus-html = { path = "../html", features = ["serialize"] }
[features] [features]
default = ["tokio_runtime"] default = ["tokio_runtime"]
@@ -31,6 +32,5 @@ tokio_runtime = ["tokio"]
[dev-dependencies] [dev-dependencies]
dioxus-html = { path = "../html" }
dioxus-hooks = { path = "../hooks" } dioxus-hooks = { path = "../hooks" }
simple_logger = "1.13.0" simple_logger = "1.13.0"
+3 -7
View File
@@ -1,7 +1,6 @@
use std::cell::RefCell; use std::cell::RefCell;
use dioxus::prelude::Scope; use dioxus::prelude::Scope;
use dioxus::ScopeChildren;
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties}; use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties};
use dioxus_core_macro::Props; use dioxus_core_macro::Props;
@@ -44,7 +43,7 @@ pub struct WebviewWindowProps<'a> {
/// focuse me /// focuse me
onfocused: &'a dyn FnMut(()), onfocused: &'a dyn FnMut(()),
children: ScopeChildren<'a>, children: Element,
} }
/// A handle to a /// 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::<RefCell<DesktopContext>>()?; let dtcx = cx.consume_state::<RefCell<DesktopContext>>()?;
cx.use_hook( cx.use_hook(
@@ -67,9 +66,6 @@ pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element<
|state| { |state| {
// //
}, },
|hook| {
//
},
); );
// render the children directly // render the children directly
@@ -94,7 +90,7 @@ fn syntax_works() {
use dioxus_hooks::*; use dioxus_hooks::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
static App: FC<()> = |(cx, props)| { static App: FC<()> = |cx, props| {
cx.render(rsx! { cx.render(rsx! {
// left window // left window
WebviewWindow { WebviewWindow {
+1
View File
@@ -0,0 +1 @@
+6 -6
View File
@@ -4,7 +4,8 @@
use std::sync::Arc; use std::sync::Arc;
use std::{any::Any, rc::Rc}; 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)] #[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent { struct ImEvent {
@@ -23,7 +24,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
contents, contents,
} = ims.into_iter().next().unwrap(); } = 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 mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let name = event_name_from_typ(&event); let name = event_name_from_typ(&event);
@@ -31,15 +32,14 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
UserEvent { UserEvent {
name, name,
event, priority: EventPriority::Low,
scope, scope_id,
mounted_dom_id, mounted_dom_id,
event,
} }
} }
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box<dyn Any + Send> { fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box<dyn Any + Send> {
use dioxus_core::events::on::*;
match name { match name {
"copy" | "cut" | "paste" => { "copy" | "cut" | "paste" => {
// //
+3 -5
View File
@@ -13,7 +13,6 @@ use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use cfg::DesktopConfig; use cfg::DesktopConfig;
use dioxus_core::scheduler::SchedulerMsg;
use dioxus_core::*; use dioxus_core::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -167,7 +166,7 @@ pub fn run<T: 'static + Send + Sync>(
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap(); sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
if let Some(BridgeEvent::Update(edits)) = rx.blocking_recv() { 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))) Some(RpcResponse::new_result(req.id.take(), Some(edits)))
} else { } else {
log::info!("none received message"); log::info!("none received message");
@@ -277,7 +276,7 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
runtime.block_on(async move { runtime.block_on(async move {
let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver); 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(); let edits = vir.rebuild();
@@ -299,14 +298,13 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
// todo: maybe we want to schedule ourselves in // todo: maybe we want to schedule ourselves in
// on average though, the virtualdom running natively is stupid fast // 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"); log::debug!("finished running with deadline");
let mut edits = vec![]; let mut edits = vec![];
while let Some(edit) = muts.pop() { 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 }) let edit_string = serde_json::to_value(Evt { edits: edit.edits })
.expect("serializing edits should never fail"); .expect("serializing edits should never fail");
edits.push(edit_string); edits.push(edit_string);
+11 -8
View File
@@ -17,6 +17,8 @@ pub(crate) struct ProvidedStateInner<T> {
impl<T> ProvidedStateInner<T> { impl<T> ProvidedStateInner<T> {
pub(crate) fn notify_consumers(&mut self) { pub(crate) fn notify_consumers(&mut self) {
for consumer in self.consumers.iter() { for consumer in self.consumers.iter() {
println!("notifiying {:?}", consumer);
// log::debug("notifiying {:?}", consumer);
(self.notify_any)(*consumer); (self.notify_any)(*consumer);
} }
} }
@@ -82,13 +84,6 @@ pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option<UseSharedStat
_ => None, _ => 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<T: 'static> {
scope_id: ScopeId, scope_id: ScopeId,
needs_notification: Cell<bool>, needs_notification: Cell<bool>,
} }
impl<T> Drop for SharedStateInner<T> {
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 struct UseSharedState<'a, T: 'static> {
pub(crate) cx: Context<'a>, 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) cx.provide_state(state)
}, },
|inner| {}, |inner| {},
|_| {},
) )
} }
-1
View File
@@ -16,7 +16,6 @@ pub fn use_ref<T: 'static>(cx: Context, f: impl FnOnce() -> T) -> UseRef<T> {
inner.update_scheuled.set(false); inner.update_scheuled.set(false);
UseRef { inner } UseRef { inner }
}, },
|_| {},
) )
} }
-1
View File
@@ -69,7 +69,6 @@ pub fn use_state<'a, T: 'static>(
UseState { inner: &*hook } UseState { inner: &*hook }
}, },
|_| {},
) )
} }
struct UseStateInner<T: 'static> { struct UseStateInner<T: 'static> {
+8 -3
View File
@@ -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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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"]
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more