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.
```rust
fn App((cx, props): Scope<()>) -> Element {
fn App(cx: Context, props: &()) -> Element {
let mut count = use_state(cx, || 0);
cx.render(rsx!(
@@ -141,16 +141,16 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
| NodeRef | ✅ | ✅ | gain direct access to nodes |
| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint |
| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions |
| Cooperative Scheduling | ✅ | ✅ | Prioritize important events over non-important events |
| Effects | ✅ | ✅ | Run effects after a component has been committed to render |
| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events |
| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server |
| Bundle Splitting | 👀 | ✅ | Efficiently and asynchronously load the app |
| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded |
| 1st class global state | ✅ | ✅ | redux/recoil/mobx on top of context |
| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) |
| 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context |
| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees |
| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts |
| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations |
| Effects | 🛠 | ✅ | Run effects after a component has been committed to render |
| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server |
| Bundle Splitting | 👀 | ✅ | Hybrid components for SPA and Server |
| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded |
| Fine-grained reactivity | 👀 | ❓ | Skip diffing for fine-grain updates |
- ✅ = implemented and working
+1 -1
View File
@@ -17,7 +17,7 @@ pub static App: FC<()> = |(cx, _)| {
let (async_count, dir) = (count.for_async(), *direction);
let (task, _) = use_task(cx, move || async move {
let (task, _) = use_coroutine(cx, move || async move {
loop {
TimeoutFuture::new(250).await;
*async_count.get_mut() += dir;
+1 -1
View File
@@ -21,7 +21,7 @@ fn main() {
}
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();
+1 -1
View File
@@ -5,7 +5,7 @@ fn main() {
dom.rebuild();
}
const App: FC<()> = |(cx, props)| {
const App: FC<()> = |cx, props| {
let id = cx.scope_id();
// cx.submit_task(Box::pin(async move { id }));
+1 -1
View File
@@ -6,7 +6,7 @@ struct SomeContext {
}
#[allow(unused)]
static Example: FC<()> = |(cx, props)| {
static Example: FC<()> = |cx, props| {
todo!()
// let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap());
+3 -3
View File
@@ -33,7 +33,7 @@ fn html_usage() {
static App2: FC<()> = |(cx, _)| cx.render(rsx!("hello world!"));
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let name = cx.use_state(|| 0);
cx.render(rsx!(div {
@@ -99,7 +99,7 @@ impl<'a> Children<'a> {
}
}
static Bapp: FC<()> = |(cx, props)| {
static Bapp: FC<()> = |cx, props| {
let name = cx.use_state(|| 0);
cx.render(rsx!(
@@ -114,7 +114,7 @@ static Bapp: FC<()> = |(cx, props)| {
))
};
static Match: FC<()> = |(cx, props)| {
static Match: FC<()> = |cx, props| {
//
let b: Box<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]
fn main() {
static App: FC<()> =
|(cx, props)| cx.render(Some(LazyNodes::new(move |f| f.text(format_args!("hello")))));
|cx, props| cx.render(Some(LazyNodes::new(move |f| f.text(format_args!("hello")))));
let mut dom = VirtualDom::new(App);
+8 -8
View File
@@ -32,7 +32,7 @@ use dioxus::prelude::*;
struct NoKeysProps {
data: std::collections::HashMap<u32, String>,
}
static AntipatternNoKeys: FC<NoKeysProps> = |(cx, props)| {
static AntipatternNoKeys: FC<NoKeysProps> = |cx, props| {
// WRONG: Make sure to add keys!
rsx!(cx, ul {
{props.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))}
@@ -54,7 +54,7 @@ static AntipatternNoKeys: FC<NoKeysProps> = |(cx, props)| {
///
/// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing
/// an API for registering shared state without the ContextProvider pattern.
static AntipatternNestedFragments: FC<()> = |(cx, props)| {
static AntipatternNestedFragments: FC<()> = |cx, props| {
// Try to avoid heavily nesting fragments
rsx!(cx,
Fragment {
@@ -82,7 +82,7 @@ static AntipatternNestedFragments: FC<()> = |(cx, props)| {
/// However, calling set_state will *not* update the current version of state in the component. This should be easy to
/// recognize from the function signature, but Dioxus will not update the "live" version of state. Calling `set_state`
/// merely places a new value in the queue and schedules the component for a future update.
static AntipatternRelyingOnSetState: FC<()> = |(cx, props)| {
static AntipatternRelyingOnSetState: FC<()> = |cx, props| {
let (state, set_state) = use_state(cx, || "Hello world").classic();
set_state("New state");
// This will return false! `state` will *still* be "Hello world"
@@ -99,7 +99,7 @@ static AntipatternRelyingOnSetState: FC<()> = |(cx, props)| {
/// - All components must start with an uppercase character
///
/// i.e.: the following component will be rejected when attempted to be used in the rsx! macro
static antipattern_component: FC<()> = |(cx, props)| todo!();
static antipattern_component: FC<()> = |cx, props| todo!();
/// Antipattern: Misusing hooks
/// ---------------------------
@@ -120,7 +120,7 @@ static antipattern_component: FC<()> = |(cx, props)| todo!();
struct MisuedHooksProps {
should_render_state: bool,
}
static AntipatternMisusedHooks: FC<MisuedHooksProps> = |(cx, props)| {
static AntipatternMisusedHooks: FC<MisuedHooksProps> = |cx, props| {
if props.should_render_state {
// do not place a hook in the conditional!
// prefer to move it out of the conditional
@@ -153,7 +153,7 @@ static AntipatternMisusedHooks: FC<MisuedHooksProps> = |(cx, props)| {
/// }
/// }
/// })
static _example: FC<()> = |(cx, props)| todo!();
static _example: FC<()> = |cx, props| todo!();
/// Antipattern: publishing components and hooks with all features enabled
/// ----------------------------------------------------------------------
@@ -171,9 +171,9 @@ static _example: FC<()> = |(cx, props)| todo!();
///
/// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific
/// libraries.
static __example: FC<()> = |(cx, props)| todo!();
static __example: FC<()> = |cx, props| todo!();
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
AntipatternNoKeys { data: std::collections::HashMap::new() }
AntipatternNestedFragments {}
+2 -2
View File
@@ -9,7 +9,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
Greeting {
@@ -25,7 +25,7 @@ struct GreetingProps {
name: &'static str,
}
static Greeting: FC<GreetingProps> = |(cx, props)| {
static Greeting: FC<GreetingProps> = |cx, props| {
cx.render(rsx! {
div {
h1 { "Hello, {props.name}!" }
+2 -2
View File
@@ -18,7 +18,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
Banner {
@@ -31,7 +31,7 @@ pub static Example: FC<()> = |(cx, props)| {
})
};
pub static Banner: FC<()> = |(cx, props)| {
pub static Banner: FC<()> = |cx, props| {
cx.render(rsx! {
div {
h1 { "This is a great banner!" }
@@ -16,7 +16,7 @@ use dioxus::prelude::*;
pub struct MyProps {
should_show: bool,
}
pub static Example0: FC<MyProps> = |(cx, props)| {
pub static Example0: FC<MyProps> = |cx, props| {
cx.render(rsx! {
div {
{props.should_show.then(|| rsx!{
@@ -39,7 +39,7 @@ pub static Example0: FC<MyProps> = |(cx, props)| {
pub struct MyProps1 {
should_show: bool,
}
pub static Example1: FC<MyProps1> = |(cx, props)| {
pub static Example1: FC<MyProps1> = |cx, props| {
cx.render(rsx! {
div {
// With matching
@@ -77,7 +77,7 @@ pub enum Color {
pub struct MyProps2 {
color: Color,
}
pub static Example2: FC<MyProps2> = |(cx, props)| {
pub static Example2: FC<MyProps2> = |cx, props| {
cx.render(rsx! {
div {
{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 mut color_index = use_state(cx, || 0);
let color = match *color_index % 2 {
+3 -3
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
fn main() {}
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
@@ -10,7 +10,7 @@ pub static Example: FC<()> = |(cx, props)| {
};
// A controlled component:
static ControlledSelect: FC<()> = |(cx, props)| {
static ControlledSelect: FC<()> = |cx, props| {
let value = use_state(cx, || String::from("Grapefruit"));
cx.render(rsx! {
select { value: "{value}", onchange: move |evt| value.set(evt.value()),
@@ -23,7 +23,7 @@ static ControlledSelect: FC<()> = |(cx, props)| {
};
// TODO - how do uncontrolled things work?
static UncontrolledSelect: FC<()> = |(cx, props)| {
static UncontrolledSelect: FC<()> = |cx, props| {
let value = use_state(cx, || String::new());
cx.render(rsx! {
+1 -1
View File
@@ -11,7 +11,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
custom_element {
+1 -1
View File
@@ -5,4 +5,4 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| cx.render(rsx! { Fragment {} });
pub static Example: FC<()> = |cx, props| cx.render(rsx! { Fragment {} });
+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).
/// However, if you get it wrong, the whole app will crash.
/// This is pretty flimsy.
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let data = get_data().unwrap();
cx.render(rsx!( div { "{data}" } ))
};
/// This is a pretty verbose way of error handling
/// However, it's still pretty good since we don't panic, just fail to render anything
static App1: FC<()> = |(cx, props)| {
static App1: FC<()> = |cx, props| {
let data = match get_data() {
Some(data) => data,
None => return None,
@@ -46,7 +46,7 @@ static App1: FC<()> = |(cx, props)| {
/// a user is logged in.
///
/// Dioxus will throw an error in the console if the None-path is ever taken.
static App2: FC<()> = |(cx, props)| {
static App2: FC<()> = |cx, props| {
let data = get_data()?;
cx.render(rsx!( div { "{data}" } ))
};
@@ -54,14 +54,14 @@ static App2: FC<()> = |(cx, props)| {
/// This is top-tier error handling since it displays a failure state.
///
/// However, the error is lacking in context.
static App3: FC<()> = |(cx, props)| match get_data() {
static App3: FC<()> = |cx, props| match get_data() {
Some(data) => cx.render(rsx!( div { "{data}" } )),
None => cx.render(rsx!( div { "Failed to load data :(" } )),
};
/// For errors that return results, it's possible to short-circuit the match-based error handling with `.ok()` which converts
/// a Result<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()?;
cx.render(rsx!( div { "{data}" } ))
};
@@ -69,7 +69,7 @@ static App4: FC<()> = |(cx, props)| {
/// This is great error handling since it displays a failure state... with context!
///
/// Hopefully you'll never need to display a screen like this. It's rather bad taste
static App5: FC<()> = |(cx, props)| match get_data_err() {
static App5: FC<()> = |cx, props| match get_data_err() {
Ok(data) => cx.render(rsx!( div { "{data}" } )),
Err(c) => cx.render(rsx!( div { "Failed to load data: {c}" } )),
};
+4 -4
View File
@@ -11,7 +11,7 @@
use dioxus::prelude::*;
// Returning multiple elements with rsx! or html!
static App1: FC<()> = |(cx, props)| {
static App1: FC<()> = |cx, props| {
cx.render(rsx! {
h1 { }
h2 { }
@@ -20,7 +20,7 @@ static App1: FC<()> = |(cx, props)| {
};
// Using the Fragment component
static App2: FC<()> = |(cx, props)| {
static App2: FC<()> = |cx, props| {
cx.render(rsx! {
Fragment {
div {}
@@ -31,7 +31,7 @@ static App2: FC<()> = |(cx, props)| {
};
// Using the `fragment` method on the NodeFactory
static App3: FC<()> = |(cx, props)| {
static App3: FC<()> = |cx, props| {
cx.render(LazyNodes::new(move |fac| {
fac.fragment_from_iter([
fac.text(format_args!("A")),
@@ -42,7 +42,7 @@ static App3: FC<()> = |(cx, props)| {
}))
};
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
App1 {}
App2 {}
+1 -1
View File
@@ -19,7 +19,7 @@ h1 {color: blue;}
p {color: red;}
"#;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
head { style { "{STYLE}" } }
body {
+2 -2
View File
@@ -10,7 +10,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
head {
style: { background_color: "powderblue" }
@@ -29,7 +29,7 @@ pub static Example: FC<()> = |(cx, props)| {
// .... technically the rsx! macro is slightly broken at the moment and allows styles not wrapped in style {}
// I haven't noticed any name collisions yet, and am tentatively leaving this behavior in..
// Don't rely on it.
static Example2: FC<()> = |(cx, props)| {
static Example2: FC<()> = |cx, props| {
cx.render(rsx! {
div { color: "red"
"hello world!"
+1 -1
View File
@@ -12,7 +12,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let example_data = use_state(cx, || 0);
let v = (0..10).map(|f| {
+4 -4
View File
@@ -7,7 +7,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
ButtonList {}
NonUpdatingEvents {}
@@ -16,7 +16,7 @@ pub static Example: FC<()> = |(cx, props)| {
};
/// We can use `set_name` in multiple closures; the closures automatically *copy* the reference to set_name.
static ButtonList: FC<()> = |(cx, props)| {
static ButtonList: FC<()> = |cx, props| {
let name = use_state(cx, || "...?");
let names = ["jack", "jill", "john", "jane"]
@@ -33,7 +33,7 @@ static ButtonList: FC<()> = |(cx, props)| {
/// This shows how listeners may be without a visible change in the display.
/// Check the console.
static NonUpdatingEvents: FC<()> = |(cx, props)| {
static NonUpdatingEvents: FC<()> = |cx, props| {
rsx!(cx, div {
button {
onclick: move |_| log::info!("Did not cause any updates!")
@@ -42,7 +42,7 @@ static NonUpdatingEvents: FC<()> = |(cx, props)| {
})
};
static DisablePropagation: FC<()> = |(cx, props)| {
static DisablePropagation: FC<()> = |cx, props| {
rsx!(cx,
div {
onclick: move |_| log::info!("event propagated to the div!")
+3 -3
View File
@@ -21,7 +21,7 @@ use dioxus::prelude::*;
// By default, components with no props are always memoized.
// A props of () is considered empty.
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div { "100% memoized!" }
})
@@ -35,7 +35,7 @@ pub struct MyProps1 {
name: String,
}
pub static Example1: FC<MyProps1> = |(cx, props)| {
pub static Example1: FC<MyProps1> = |cx, props| {
cx.render(rsx! {
div { "100% memoized! {props.name}" }
})
@@ -49,7 +49,7 @@ pub struct MyProps2 {
name: std::rc::Rc<str>,
}
pub static Example2: FC<MyProps2> = |(cx, props)| {
pub static Example2: FC<MyProps2> = |cx, props| {
cx.render(rsx! {
div { "100% memoized! {props.name}" }
})
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
fn main() {}
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let p = 10;
cx.render(rsx! {
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
fn main() {}
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
+1 -1
View File
@@ -9,7 +9,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let props = MyProps {
count: 0,
live: true,
+1 -1
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
fn main() {}
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
+1 -1
View File
@@ -14,7 +14,7 @@ struct DogApi {
}
const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let doggo = use_suspense(
cx,
|| surf::get(ENDPOINT).recv_json::<DogApi>(),
+1 -1
View File
@@ -24,7 +24,7 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let count = use_state(cx, || 0);
let mut direction = use_state(cx, || 1);
+1 -1
View File
@@ -1,6 +1,6 @@
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
cx.render(rsx! {
div {
+2 -2
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
use dioxus::ssr;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let as_string = use_state(cx, || {
// Currently, SSR is only supported for whole VirtualDOMs
// This is an easy/low hanging fruit to improve upon
@@ -15,7 +15,7 @@ pub static Example: FC<()> = |(cx, props)| {
})
};
static SomeApp: FC<()> = |(cx, props)| {
static SomeApp: FC<()> = |cx, props| {
cx.render(rsx! {
div { style: {background_color: "blue"}
h1 {"Some amazing app or component"}
+3 -3
View File
@@ -27,18 +27,18 @@ fn main() {
use dioxus::prelude::*;
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let p1 = use_state(cx, || 0);
let p2 = use_state(cx, || 0);
let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async());
let (p1_handle, _) = use_task(cx, || async move {
let (p1_handle, _) = use_coroutine(cx, || async move {
loop {
*p1_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(75)).await;
}
});
let (p2_handle, _) = use_task(cx, || async move {
let (p2_handle, _) = use_coroutine(cx, || async move {
loop {
*p2_async.get_mut() += 1;
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
+13 -11
View File
@@ -90,15 +90,17 @@ static App: FC<()> = |(cx, _)| {
}
};
rsx!(cx, body {
link {
rel: "stylesheet"
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css"
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5"
crossorigin: "anonymous"
}
margin_left: "35%"
h1 {"Dioxus CRM Example"}
{scene}
})
cx.render(rsx!(
body {
link {
rel: "stylesheet"
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css"
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5"
crossorigin: "anonymous"
}
margin_left: "35%"
h1 {"Dioxus CRM Example"}
{scene}
}
))
};
+1 -1
View File
@@ -8,7 +8,7 @@ fn main() {
dioxus_desktop::launch(App, |c| c);
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
div {
"hello world!"
+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 files = file_manager.read();
+1 -1
View File
@@ -19,7 +19,7 @@ fn main() {
dioxus::desktop::launch(App, |c| c.with_prerendered(content));
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let mut val = use_state(cx, || 0);
cx.render(rsx! {
+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 clear_display = state.read().display_value.eq("0");
+1 -1
View File
@@ -7,7 +7,7 @@ fn main() {
dioxus::desktop::launch(App, |c| c);
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
+1 -1
View File
@@ -23,7 +23,7 @@ pub enum Route {
NotFound,
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let route = use_router(cx, Route::parse)?;
cx.render(rsx! {
+10 -6
View File
@@ -49,7 +49,7 @@ const NONE_ELEMENT: Option<()> = None;
use baller::Baller;
use dioxus::prelude::*;
pub static Example: FC<()> = |(cx, props)| {
pub static Example: FC<()> = |cx, props| {
let formatting = "formatting!";
let formatting_tuple = ("a", "b");
let lazy_fmt = format_args!("lazily formatted text");
@@ -102,12 +102,9 @@ pub static Example: FC<()> = |(cx, props)| {
}}
// Matching
// Matching will throw a Rust error about "no two closures are the same type"
// To fix this, call "render" method or use the "in" syntax to produce VNodes.
// There's nothing we can do about it, sorry :/ (unless you want *really* unhygienic macros)
{match true {
true => rsx!(cx, h1 {"Top text"}),
false => cx.render(rsx!( h1 {"Bottom text"}))
true => rsx!( h1 {"Top text"}),
false => rsx!( h1 {"Bottom text"})
}}
// Conditional rendering
@@ -174,10 +171,17 @@ pub static Example: FC<()> = |(cx, props)| {
// Can take children too!
Taller { a: "asd", div {"hello world!"} }
// helper functions
{helper(cx, "hello world!")}
}
})
};
fn helper(cx: Context, text: &str) -> Element {
rsx!(cx, p { "{text}" })
}
mod baller {
use super::*;
#[derive(Props, PartialEq)]
+2 -2
View File
@@ -9,7 +9,7 @@ fn main() {
println!("{}", ssr::render_vdom(&vdom, |c| c));
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
div {
h1 { "Title" }
@@ -21,6 +21,6 @@ static App: FC<()> = |(cx, props)| {
struct MyProps<'a> {
text: &'a str,
}
fn App2<'a>(cx: Context<'a>, props: &'a MyProps) -> Element<'a> {
fn App2(cx: Context, props: &MyProps) -> Element {
None
}
+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!(
div {
class: "overflow-hidden"
+1 -1
View File
@@ -42,7 +42,7 @@ fn main() {}
// 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());
// cx.render(rsx! {
+6 -6
View File
@@ -24,7 +24,7 @@ fn main() {
.unwrap();
}
pub static App: FC<()> = |(cx, props)| {
pub static App: FC<()> = |cx, props| {
cx.render(rsx!(
div { class: "overflow-hidden"
link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" }
@@ -39,7 +39,7 @@ pub static App: FC<()> = |(cx, props)| {
))
};
pub static Header: FC<()> = |(cx, props)| {
pub static Header: FC<()> = |cx, props| {
cx.render(rsx! {
div {
header { class: "text-gray-400 bg-gray-900 body-font"
@@ -65,7 +65,7 @@ pub static Header: FC<()> = |(cx, props)| {
})
};
pub static Hero: FC<()> = |(cx, props)| {
pub static Hero: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -103,7 +103,7 @@ pub static Hero: FC<()> = |(cx, props)| {
}
})
};
pub static Entry: FC<()> = |(cx, props)| {
pub static Entry: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -116,7 +116,7 @@ pub static Entry: FC<()> = |(cx, props)| {
})
};
pub static StacksIcon: FC<()> = |(cx, props)| {
pub static StacksIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
xmlns: "http://www.w3.org/2000/svg"
@@ -131,7 +131,7 @@ pub static StacksIcon: FC<()> = |(cx, props)| {
}
))
};
pub static RightArrowIcon: FC<()> = |(cx, props)| {
pub static RightArrowIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
fill: "none"
+6 -6
View File
@@ -14,7 +14,7 @@ fn main() {
const STYLE: &str = "body {overflow:hidden;}";
pub static App: FC<()> = |(cx, props)| {
pub static App: FC<()> = |cx, props| {
cx.render(rsx!(
div { class: "overflow-hidden"
style { "{STYLE}" }
@@ -30,7 +30,7 @@ pub static App: FC<()> = |(cx, props)| {
))
};
pub static Header: FC<()> = |(cx, props)| {
pub static Header: FC<()> = |cx, props| {
cx.render(rsx! {
div {
header { class: "text-gray-400 bg-gray-900 body-font"
@@ -56,7 +56,7 @@ pub static Header: FC<()> = |(cx, props)| {
})
};
pub static Hero: FC<()> = |(cx, props)| {
pub static Hero: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -94,7 +94,7 @@ pub static Hero: FC<()> = |(cx, props)| {
}
})
};
pub static Entry: FC<()> = |(cx, props)| {
pub static Entry: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -107,7 +107,7 @@ pub static Entry: FC<()> = |(cx, props)| {
})
};
pub static StacksIcon: FC<()> = |(cx, props)| {
pub static StacksIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
// xmlns: "http://www.w3.org/2000/svg"
@@ -122,7 +122,7 @@ pub static StacksIcon: FC<()> = |(cx, props)| {
}
))
};
pub static RightArrowIcon: FC<()> = |(cx, props)| {
pub static RightArrowIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
fill: "none"
+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 App: FC<()> = |(cx, props)| {
const App: FC<()> = |cx, props| {
let mut draft = use_state(cx, || "".to_string());
let mut todos = use_state(cx, || HashMap::<u32, Rc<TodoItem>>::new());
let mut filter = use_state(cx, || FilterState::All);
@@ -85,7 +85,7 @@ pub struct TodoEntryProps {
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 contents = use_state(cx, || String::from(""));
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";
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
//
let body = use_suspense(
cx,
@@ -24,21 +24,23 @@ static App: FC<()> = |(cx, props)| {
.await
.unwrap();
},
|cx, props| {
|props| {
//
rsx!(WeatherDisplay {})
cx.render(rsx!(WeatherDisplay {}))
},
);
rsx!(cx, div {
{body}
cx.render(rsx! {
div {
{body}
}
})
};
#[derive(PartialEq, Props)]
struct WeatherProps {}
static WeatherDisplay: FC<WeatherProps> = |(cx, props)| {
static WeatherDisplay: FC<WeatherProps> = |cx, props| {
//
cx.render(rsx!(
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)
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let mut state = use_state(cx, || 0);
cx.render(rsx! {
div {
+2 -2
View File
@@ -23,7 +23,7 @@ fn main() {
// dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
dbg!("rednering parent");
cx.render(rsx! {
div {
@@ -40,7 +40,7 @@ static App: FC<()> = |(cx, props)| {
})
};
static But: FC<()> = |(cx, props)| {
static But: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
// let d = Dropper { name: "asd" };
+6 -6
View File
@@ -24,7 +24,7 @@ fn main() {
dioxus_web::launch(App, |c| c)
}
pub static App: FC<()> = |(cx, props)| {
pub static App: FC<()> = |cx, props| {
cx.render(rsx!(
div { class: "overflow-hidden"
link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" }
@@ -39,7 +39,7 @@ pub static App: FC<()> = |(cx, props)| {
))
};
pub static Header: FC<()> = |(cx, props)| {
pub static Header: FC<()> = |cx, props| {
cx.render(rsx! {
div {
header { class: "text-gray-400 bg-gray-900 body-font"
@@ -65,7 +65,7 @@ pub static Header: FC<()> = |(cx, props)| {
})
};
pub static Hero: FC<()> = |(cx, props)| {
pub static Hero: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -103,7 +103,7 @@ pub static Hero: FC<()> = |(cx, props)| {
}
})
};
pub static Entry: FC<()> = |(cx, props)| {
pub static Entry: FC<()> = |cx, props| {
//
cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font"
@@ -116,7 +116,7 @@ pub static Entry: FC<()> = |(cx, props)| {
})
};
pub static StacksIcon: FC<()> = |(cx, props)| {
pub static StacksIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
// xmlns: "http://www.w3.org/2000/svg"
@@ -131,7 +131,7 @@ pub static StacksIcon: FC<()> = |(cx, props)| {
}
))
};
pub static RightArrowIcon: FC<()> = |(cx, props)| {
pub static RightArrowIcon: FC<()> = |cx, props| {
cx.render(rsx!(
svg {
fill: "none"
+1 -1
View File
@@ -19,7 +19,7 @@ fn main() {
dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let mut rng = SmallRng::from_entropy();
let rows = (0..1_000).map(|f| {
let label = Label::new(&mut rng);
+1 -1
View File
@@ -16,7 +16,7 @@ fn main() {
dioxus::web::launch(App, |c| c);
}
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
+1 -1
View File
@@ -173,7 +173,7 @@ impl ToTokens for Component {
if !self.children.is_empty() {
let childs = &self.children;
toks.append_all(quote! {
.children(ScopeChildren::new(__cx.fragment_from_iter([ #( #childs ),* ])))
.children(__cx.create_children([ #( #childs ),* ]))
});
}
+2 -2
View File
@@ -210,12 +210,12 @@ impl ToTokens for ElementAttrNamed {
ElementAttr::EventClosure { name, closure } => {
quote! {
dioxus::events::on::#name(__cx, #closure)
dioxus_elements::on::#name(__cx, #closure)
}
}
ElementAttr::EventTokens { name, tokens } => {
quote! {
dioxus::events::on::#name(__cx, #tokens)
dioxus_elements::on::#name(__cx, #tokens)
}
}
};
+2 -3
View File
@@ -40,8 +40,6 @@ indexmap = "1.7.0"
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1.7", optional = true }
[dev-dependencies]
anyhow = "1.0.42"
dioxus-html = { path = "../html" }
@@ -49,12 +47,13 @@ fern = { version = "0.6.0", features = ["colored"] }
rand = { version = "0.8.4", features = ["small_rng"] }
simple_logger = "1.13.0"
dioxus-core-macro = { path = "../core-macro", version = "0.1.2" }
dioxus-hooks = { path = "../hooks" }
# async-std = { version = "1.9.0", features = ["attributes"] }
# criterion = "0.3.5"
[features]
default = []
serialize = ["serde", "serde_repr"]
serialize = ["serde"]
debug_vdom = []
[[bench]]
+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 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
10. return the patches back to the event loop
11. apply the patches to the real dom
/*
Welcome to Dioxus's cooperative, priority-based scheduler.
I hope you enjoy your stay.
Some essential reading:
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
- https://github.com/WICG/is-input-pending
- https://web.dev/rail/
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
# What's going on?
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
point in being "fast" if you can't also be "responsive."
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
different types of work simultaneously.
# How does it work?
Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
React, and the UI still stays snappy!
While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
has time to paint and run animations. When the browser is finished, we can step in and continue our work.
React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
future is paused and saved (self-referentially).
# Priority System
So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work.
React's priority system is quite complex.
There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
For Dioxus, we're going with a 4 tier priority system:
- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
- High: for events that block all others - clicks, keyboard, and hovers
- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
- Low: set_state called asynchronously, and anything generated by suspense
In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
do anything too arduous from onInput.
For the rest, we defer to the rIC period and work down each queue from high to low.
*/
Strategy:
- When called, check for any UI events that might've been received since the last frame.
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high,"
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
'static components have a leg up here since their work can be re-used among multiple scopes.
"High priority" is only for blocking! Should only be used on "clicks"
- If there are no pending discrete events, then check for continuous events. These can be completely batched
- we batch completely until we run into a discrete event
- all continuous events are batched together
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
- we need to keep track of element parents to be able to traverse properly
Open questions:
- what if we get two clicks from the component during the same slice?
- should we batch?
- react says no - they are continuous
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
+2 -2
View File
@@ -24,7 +24,7 @@ criterion_group!(mbenches, create_rows);
criterion_main!(mbenches);
fn create_rows(c: &mut Criterion) {
static App: FC<()> = |(cx, _)| {
static App: FC<()> = |cx, _| {
let mut rng = SmallRng::from_entropy();
let rows = (0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
@@ -58,7 +58,7 @@ struct RowProps {
row_id: usize,
label: Label,
}
fn Row((cx, props): Scope<RowProps>) -> Element {
fn Row(cx: Context, props: &RowProps) -> Element {
let [adj, col, noun] = props.label.0;
cx.render(rsx! {
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
//! that ensures compile-time required and optional fields on cx.
use crate::{
innerlude::{Context, Element, VAnchor, VFragment, VNode},
LazyNodes, ScopeChildren,
};
/// A component is a wrapper around a Context and some Props that share a lifetime
///
///
/// # Example
///
/// With memoized state:
/// ```rust
/// struct State {}
///
/// fn Example((cx, props): Scope<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);
use crate::innerlude::{Context, Element, LazyNodes, NodeLink};
pub struct FragmentProps<'a> {
children: ScopeChildren<'a>,
pub struct FragmentProps(Element);
pub struct FragmentBuilder<const BUILT: bool>(Element);
impl FragmentBuilder<false> {
pub fn children(self, children: Option<NodeLink>) -> FragmentBuilder<true> {
FragmentBuilder(children)
}
}
pub struct FragmentBuilder<'a, const BUILT: bool> {
children: Option<ScopeChildren<'a>>,
}
impl<'a> FragmentBuilder<'a, false> {
pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> {
FragmentBuilder {
children: Some(children),
}
impl<const A: bool> FragmentBuilder<A> {
pub fn build(self) -> FragmentProps {
FragmentProps(self.0)
}
}
impl<'a, const A: bool> FragmentBuilder<'a, A> {
pub fn build(self) -> FragmentProps<'a> {
FragmentProps {
children: self.children.unwrap_or_default(),
}
}
}
impl<'a> Properties for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
/// Access the children elements passed into the component
///
/// This enables patterns where a component is passed children from its parent.
///
/// ## Details
///
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
/// on the props that takes Context.
///
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
/// props are valid for the static lifetime.
///
/// ## Example
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
/// p {}
/// }
/// })
/// }
///
/// #[derive(PartialEq, Props)]
/// struct CardProps {
/// children: Element
/// }
///
/// fn CustomCard(cx: Context, props: &CardProps) -> Element {
/// cx.render(rsx!{
/// div {
/// h1 {"Title card"}
/// {props.children}
/// }
/// })
/// }
/// ```
impl Properties for FragmentProps {
type Builder = FragmentBuilder<false>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
FragmentBuilder { children: None }
FragmentBuilder(None)
}
unsafe fn memoize(&self, other: &Self) -> bool {
unsafe fn memoize(&self, _other: &Self) -> bool {
false
}
}
/// Create inline fragments using Component syntax.
///
/// ## Details
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// # Example
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
/// a key is needed to identify each item.
///
/// ## Example
///
/// ```rust
/// rsx!{
@@ -92,20 +88,17 @@ impl<'a> Properties for FragmentProps<'a> {
/// }
/// ```
///
/// # Details
/// ## Usage
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid nesting fragments if you can. There is no protection against infinitely nested fragments.
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
///
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>((cx, props): Scope<'a, FragmentProps<'a>>) -> Element<'a> {
cx.render(Some(LazyNodes::new(|f| {
f.fragment_from_iter(&props.children)
})))
pub fn Fragment(cx: Context, props: &FragmentProps) -> Element {
cx.render(Some(LazyNodes::new(|f| f.fragment_from_iter(&props.0))))
}
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
@@ -167,12 +160,11 @@ impl Properties for () {
// that the macros use to anonymously complete prop construction.
pub struct EmptyBuilder;
impl EmptyBuilder {
#[inline]
pub fn build(self) {}
}
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Context<'a>, &'a T) -> Element) -> T::Builder {
T::builder()
}
-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 fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
use DomEdit::*;
/// Our DiffMachine is an iterative tree differ.
@@ -104,76 +105,144 @@ use DomEdit::*;
///
/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's
/// stack machines all the way down!
pub(crate) struct DiffMachine<'bump> {
pub vdom: &'bump ResourcePool,
pub struct DiffState<'bump> {
scopes: &'bump ScopeArena,
pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>,
pub(crate) stack: DiffStack<'bump>,
pub seen_scopes: FxHashSet<ScopeId>,
pub cfg: DiffCfg,
}
pub(crate) struct DiffCfg {
pub force_diff: bool,
}
impl Default for DiffCfg {
fn default() -> Self {
impl<'bump> DiffState<'bump> {
pub(crate) fn new(scopes: &'bump ScopeArena) -> Self {
Self {
force_diff: Default::default(),
}
}
}
/// a "saved" form of a diff machine
/// in regular diff machine, the &'bump reference is a stack borrow, but the
/// bump lifetimes are heap borrows.
pub(crate) struct SavedDiffWork<'bump> {
pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>,
pub seen_scopes: FxHashSet<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(),
scopes,
mutations: Mutations::new(),
stack: DiffStack::new(),
vdom: shared,
seen_scopes: FxHashSet::default(),
seen_scopes: Default::default(),
force_diff: false,
}
}
}
/// The stack instructions we use to diff and create new nodes.
#[derive(Debug)]
pub(crate) enum DiffInstruction<'a> {
Diff {
old: &'a VNode<'a>,
new: &'a VNode<'a>,
},
Create {
node: &'a VNode<'a>,
},
/// pushes the node elements onto the stack for use in mount
PrepareMove {
node: &'a VNode<'a>,
},
Mount {
and: MountType<'a>,
},
PopScope,
}
#[derive(Debug, Clone, Copy)]
pub enum MountType<'a> {
Absorb,
Append,
Replace { old: &'a VNode<'a> },
InsertAfter { other_node: &'a VNode<'a> },
InsertBefore { other_node: &'a VNode<'a> },
}
pub(crate) struct DiffStack<'bump> {
pub(crate) instructions: Vec<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> {
SavedDiffWork {
mutations: self.mutations,
stack: self.stack,
seen_scopes: self.seen_scopes,
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 diff_scope(&mut self, id: ScopeId) {
if let Some(component) = self.vdom.get_scope_mut(id) {
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
self.stack.push(DiffInstruction::Diff { new, old });
self.work(|| false);
}
pub fn push_subtree(&mut self) {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount {
and: MountType::Append,
});
}
pub fn push_nodes_created(&mut self, count: usize) {
self.nodes_created_stack.push(count);
}
pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and });
self.instructions.push(DiffInstruction::Create { node });
}
pub fn add_child_count(&mut self, count: usize) {
*self.nodes_created_stack.last_mut().unwrap() += count;
}
pub fn pop_nodes_created(&mut self) -> usize {
self.nodes_created_stack.pop().unwrap()
}
pub fn current_scope(&self) -> Option<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"
@@ -189,7 +258,10 @@ impl<'bump> DiffMachine<'bump> {
DiffInstruction::Diff { old, new } => self.diff_node(old, new),
DiffInstruction::Create { node } => self.create_node(node),
DiffInstruction::Mount { and } => self.mount(and),
DiffInstruction::PrepareMove { node } => self.prepare_move_node(node),
DiffInstruction::PrepareMove { node } => {
let num_on_stack = self.push_all_nodes(node);
self.stack.add_child_count(num_on_stack);
}
DiffInstruction::PopScope => self.stack.pop_off_scope(),
};
@@ -202,10 +274,38 @@ impl<'bump> DiffMachine<'bump> {
true
}
fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) {
for el in RealChildIterator::new(node, self.vdom) {
self.mutations.push_root(el.mounted_id());
self.stack.add_child_count(1);
// recursively push all the nodes of a tree onto the stack and return how many are there
fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize {
match node {
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
self.mutations.push_root(node.mounted_id());
1
}
VNode::Linked(linked) => {
let node = unsafe { &*linked.node };
let node: &VNode = unsafe { std::mem::transmute(node) };
self.push_all_nodes(node)
}
VNode::Fragment(_) | VNode::Component(_) => {
//
let mut added = 0;
for child in node.children() {
added += self.push_all_nodes(child);
}
added
}
VNode::Element(el) => {
let mut num_on_stack = 0;
for child in el.children.iter() {
num_on_stack += self.push_all_nodes(child);
}
self.mutations.push_root(el.dom_id.get().unwrap());
num_on_stack + 1
}
}
}
@@ -219,15 +319,7 @@ impl<'bump> DiffMachine<'bump> {
}
MountType::Replace { old } => {
if let Some(old_id) = old.try_mounted_id() {
self.mutations.replace_with(old_id, nodes_created as u32);
self.remove_nodes(Some(old), true);
} else {
if let Some(id) = self.find_first_element_id(old) {
self.mutations.replace_with(id, nodes_created as u32);
}
self.remove_nodes(Some(old), true);
}
self.replace_node(old, nodes_created);
}
MountType::Append => {
@@ -260,18 +352,19 @@ impl<'bump> DiffMachine<'bump> {
VNode::Element(element) => self.create_element_node(element, node),
VNode::Fragment(frag) => self.create_fragment_node(frag),
VNode::Component(component) => self.create_component_node(component),
VNode::Linked(linked) => self.create_linked_node(linked),
}
}
fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
let real_id = self.vdom.reserve_node(node);
let real_id = self.scopes.reserve_node(node);
self.mutations.create_text_node(vtext.text, real_id);
vtext.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
}
fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) {
let real_id = self.vdom.reserve_node(node);
let real_id = self.scopes.reserve_node(node);
self.mutations.create_placeholder(real_id);
suspended.dom_id.set(Some(real_id));
@@ -281,9 +374,11 @@ impl<'bump> DiffMachine<'bump> {
}
fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) {
let real_id = self.vdom.reserve_node(node);
let real_id = self.scopes.reserve_node(node);
self.mutations.create_placeholder(real_id);
anchor.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
}
@@ -298,7 +393,7 @@ impl<'bump> DiffMachine<'bump> {
..
} = element;
let real_id = self.vdom.reserve_node(node);
let real_id = self.scopes.reserve_node(node);
dom_id.set(Some(real_id));
@@ -307,13 +402,13 @@ impl<'bump> DiffMachine<'bump> {
self.stack.add_child_count(1);
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
let scope = self.scopes.get_scope(&cur_scope_id).unwrap();
listeners.iter().for_each(|listener| {
for listener in *listeners {
self.attach_listener_to_scope(listener, scope);
listener.mounted_node.set(Some(real_id));
self.mutations.new_event_listener(listener, cur_scope_id);
});
}
} else {
log::warn!("create element called with no scope on the stack - this is an error for a live dom");
}
@@ -332,61 +427,47 @@ impl<'bump> DiffMachine<'bump> {
}
fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) {
let caller = vcomponent.caller;
let parent_idx = self.stack.current_scope().unwrap();
let shared = self.vdom.channel.clone();
// Insert a new scope into our component list
let parent_scope = self.vdom.get_scope(parent_idx).unwrap();
let parent_scope = self.scopes.get_scope(&parent_idx).unwrap();
let height = parent_scope.height + 1;
let subtree = parent_scope.subtree.get();
let new_idx = self.vdom.insert_scope_with_key(|new_idx| {
ScopeInner::new(
caller,
new_idx,
Some(parent_idx),
parent_scope.height + 1,
parent_scope.subtree(),
shared,
)
});
let parent_scope = unsafe { self.scopes.get_scope_raw(&parent_idx) };
let caller = unsafe { std::mem::transmute(vcomponent.caller as *const _) };
let fc_ptr = vcomponent.user_fc;
let new_idx = self
.scopes
.new_with_key(fc_ptr, caller, parent_scope, height, subtree);
// Actually initialize the caller's slot with the right address
vcomponent.associated_scope.set(Some(new_idx));
if !vcomponent.can_memoize {
let cur_scope = self.vdom.get_scope_mut(parent_idx).unwrap();
let extended = vcomponent as *const VComponent;
let extended: *const VComponent<'static> = unsafe { std::mem::transmute(extended) };
cur_scope.borrowed_props.borrow_mut().push(extended);
let cur_scope = self.scopes.get_scope(&parent_idx).unwrap();
let extended = unsafe { std::mem::transmute(vcomponent) };
cur_scope.items.borrow_mut().borrowed_props.push(extended);
}
// TODO:
// add noderefs to current noderef list Noderefs
// add effects to current effect list Effects
let new_component = self.vdom.get_scope_mut(new_idx).unwrap();
let new_component = self.scopes.get_scope(&new_idx).unwrap();
log::debug!(
"initializing component {:?} with height {:?}",
new_idx,
parent_scope.height + 1
height + 1
);
// Run the scope for one iteration to initialize it
if new_component.run_scope(self.vdom) {
if self.scopes.run_scope(&new_idx) {
// Take the node that was just generated from running the component
let nextnode = new_component.frames.fin_head();
let nextnode = self.scopes.fin_head(&new_idx);
self.stack.create_component(new_idx, nextnode);
//
/*
tree_item {
}
*/
if new_component.is_subtree_root.get() {
self.stack.push_subtree();
}
@@ -396,6 +477,16 @@ impl<'bump> DiffMachine<'bump> {
self.seen_scopes.insert(new_idx);
}
fn create_linked_node(&mut self, link: &'bump NodeLink) {
if link.scope_id.get().is_none() {
if let Some(cur_scope) = self.stack.current_scope() {
link.scope_id.set(Some(cur_scope));
}
}
let node: &'bump VNode<'static> = unsafe { &*link.node };
self.create_node(unsafe { std::mem::transmute(node) });
}
// =================================
// Tools for diffing nodes
// =================================
@@ -412,11 +503,14 @@ impl<'bump> DiffMachine<'bump> {
(Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()),
(Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new),
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
(Linked(old), Linked(new)) => self.diff_linked_nodes(old, new),
// Anything else is just a basic replace and create
(
Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_),
Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_),
Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
| Suspended(_),
Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
| Suspended(_),
) => self
.stack
.create_node(new_node, MountType::Replace { old: old_node }),
@@ -494,7 +588,7 @@ impl<'bump> DiffMachine<'bump> {
//
// TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
let scope = self.scopes.get_scope(&cur_scope_id).unwrap();
if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
@@ -533,33 +627,42 @@ impl<'bump> DiffMachine<'bump> {
&mut self,
old_node: &'bump VNode<'bump>,
new_node: &'bump VNode<'bump>,
old: &'bump VComponent<'bump>,
new: &'bump VComponent<'bump>,
) {
let scope_addr = old.associated_scope.get().unwrap();
log::debug!(
"Diffing components. old_scope: {:?}, old_addr: {:?}, new_addr: {:?}",
scope_addr,
old.user_fc,
new.user_fc
);
// Make sure we're dealing with the same component (by function pointer)
if old.user_fc == new.user_fc {
log::debug!("Diffing component {:?} - {:?}", new.user_fc, scope_addr);
//
self.stack.scope_stack.push(scope_addr);
// Make sure the new component vnode is referencing the right scope id
new.associated_scope.set(Some(scope_addr));
// make sure the component's caller function is up to date
let scope = self.vdom.get_scope_mut(scope_addr).unwrap();
scope.update_scope_dependencies(new.caller);
let scope = unsafe {
self.scopes
.get_scope_mut(&scope_addr)
.expect(&format!("could not find {:?}", scope_addr))
};
scope.caller = unsafe { std::mem::transmute(new.caller) };
// React doesn't automatically memoize, but we do.
let props_are_the_same = old.comparator.unwrap();
if self.cfg.force_diff || !props_are_the_same(new) {
let succeeded = scope.run_scope(self.vdom);
if succeeded {
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
if self.force_diff || !props_are_the_same(new) {
if self.scopes.run_scope(&scope_addr) {
self.diff_node(
self.scopes.wip_head(&scope_addr),
self.scopes.fin_head(&scope_addr),
);
}
}
@@ -589,6 +692,19 @@ impl<'bump> DiffMachine<'bump> {
self.attach_suspended_node_to_scope(new);
}
fn diff_linked_nodes(&mut self, old: &'bump NodeLink, new: &'bump NodeLink) {
if !std::ptr::eq(old.node, new.node) {
// if the ptrs are the same then theyr're the same
let old: &VNode = unsafe { std::mem::transmute(&*old.node) };
let new: &VNode = unsafe { std::mem::transmute(&*new.node) };
self.diff_node(old, new);
}
if new.scope_id.get().is_none() {
todo!("attach the link to the scope - when children are not created");
}
}
// =============================================
// Utilities for creating new diff instructions
// =============================================
@@ -627,7 +743,14 @@ impl<'bump> DiffMachine<'bump> {
.create_children(new, MountType::Replace { old: &old[0] });
}
(_, [VNode::Anchor(_)]) => {
self.replace_and_create_many_with_one(old, &new[0]);
let new: &'bump VNode<'bump> = &new[0];
if let Some(first_old) = old.get(0) {
self.remove_nodes(&old[1..], true);
self.stack
.create_node(new, MountType::Replace { old: first_old });
} else {
self.stack.create_node(new, MountType::Append {});
}
}
_ => {
let new_is_keyed = new[0].key().is_some();
@@ -799,6 +922,7 @@ impl<'bump> DiffMachine<'bump> {
/// If there is no offset, then this function returns None and the diffing is complete.
fn diff_keyed_ends(
&mut self,
old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>],
) -> Option<(usize, usize)> {
@@ -858,7 +982,7 @@ impl<'bump> DiffMachine<'bump> {
//
// This function will load the appropriate nodes onto the stack and do diffing in place.
//
// Upon exit from this function, it will be restored to that same state.
// Upon exit from this function, it will be restored to that same self.
fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) {
/*
1. Map the old keys into a numerical ordering based on indices.
@@ -923,7 +1047,14 @@ impl<'bump> DiffMachine<'bump> {
log::debug!("old_key_to_old_index, {:#?}", old_key_to_old_index);
log::debug!("new_index_to_old_index, {:#?}", new_index_to_old_index);
log::debug!("shared_keys, {:#?}", shared_keys);
self.replace_and_create_many_with_many(old, new);
if let Some(first_old) = old.get(0) {
self.remove_nodes(&old[1..], true);
self.stack
.create_children(new, MountType::Replace { old: first_old })
} else {
self.stack.create_children(new, MountType::Append {});
}
return;
}
@@ -1032,14 +1163,16 @@ impl<'bump> DiffMachine<'bump> {
VNode::Element(t) => break t.dom_id.get(),
VNode::Suspended(t) => break t.dom_id.get(),
VNode::Anchor(t) => break t.dom_id.get(),
VNode::Linked(l) => {
let node: &VNode = unsafe { std::mem::transmute(&*l.node) };
self.find_last_element(node);
}
VNode::Fragment(frag) => {
search_node = frag.children.last();
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
search_node = Some(scope.root_node());
search_node = Some(self.scopes.root_node(&scope_id));
}
}
}
@@ -1056,8 +1189,11 @@ impl<'bump> DiffMachine<'bump> {
}
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
search_node = Some(scope.root_node());
search_node = Some(self.scopes.root_node(&scope_id));
}
VNode::Linked(link) => {
let node = unsafe { std::mem::transmute(&*link.node) };
search_node = Some(node);
}
VNode::Text(t) => break t.dom_id.get(),
VNode::Element(t) => break t.dom_id.get(),
@@ -1067,17 +1203,38 @@ impl<'bump> DiffMachine<'bump> {
}
}
fn replace_and_create_many_with_one(
&mut self,
old: &'bump [VNode<'bump>],
new: &'bump VNode<'bump>,
) {
if let Some(first_old) = old.get(0) {
self.remove_nodes(&old[1..], true);
self.stack
.create_node(new, MountType::Replace { old: first_old });
} else {
self.stack.create_node(new, MountType::Append {});
fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) {
match old {
VNode::Element(el) => {
let id = old.try_mounted_id().expect(&format!("broke on {:?}", old));
self.mutations.replace_with(id, nodes_created as u32);
self.remove_nodes(el.children, false);
}
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
let id = old.try_mounted_id().expect(&format!("broke on {:?}", old));
self.mutations.replace_with(id, nodes_created as u32);
}
VNode::Fragment(f) => {
self.replace_node(&f.children[0], nodes_created);
self.remove_nodes(f.children.iter().skip(1), true);
}
VNode::Component(c) => {
let node = self.scopes.fin_head(&c.associated_scope.get().unwrap());
self.replace_node(node, nodes_created);
let scope_id = c.associated_scope.get().unwrap();
println!("replacing c {:?} ", scope_id);
log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(&scope_id).unwrap();
}
VNode::Linked(l) => {
let node: &'bump VNode<'bump> = unsafe { std::mem::transmute(&*l.node) };
self.replace_node(node, nodes_created);
}
}
}
@@ -1092,16 +1249,18 @@ impl<'bump> DiffMachine<'bump> {
for node in nodes {
match node {
VNode::Text(t) => {
let id = t.dom_id.get().unwrap();
self.vdom.collect_garbage(id);
// this check exists because our null node will be removed but does not have an ID
if let Some(id) = t.dom_id.get() {
self.scopes.collect_garbage(id);
if gen_muts {
self.mutations.remove(id.as_u64());
if gen_muts {
self.mutations.remove(id.as_u64());
}
}
}
VNode::Suspended(s) => {
let id = s.dom_id.get().unwrap();
self.vdom.collect_garbage(id);
self.scopes.collect_garbage(id);
if gen_muts {
self.mutations.remove(id.as_u64());
@@ -1109,7 +1268,7 @@ impl<'bump> DiffMachine<'bump> {
}
VNode::Anchor(a) => {
let id = a.dom_id.get().unwrap();
self.vdom.collect_garbage(id);
self.scopes.collect_garbage(id);
if gen_muts {
self.mutations.remove(id.as_u64());
@@ -1129,56 +1288,45 @@ impl<'bump> DiffMachine<'bump> {
self.remove_nodes(f.children, gen_muts);
}
VNode::Component(c) => {
let scope_id = c.associated_scope.get().unwrap();
let scope = self.vdom.get_scope_mut(scope_id).unwrap();
let root = scope.root_node();
self.remove_nodes(Some(root), gen_muts);
VNode::Linked(l) => {
let node = unsafe { std::mem::transmute(&*l.node) };
self.remove_nodes(Some(node), gen_muts);
}
log::debug!("Destroying scope {:?}", scope_id);
let mut s = self.vdom.try_remove(scope_id).unwrap();
s.hooks.clear_hooks();
VNode::Component(c) => {
self.destroy_vomponent(c, gen_muts);
}
}
}
}
/// Remove all the old nodes and replace them with newly created new nodes.
///
/// The new nodes *will* be created - don't create them yourself!
fn replace_and_create_many_with_many(
&mut self,
old: &'bump [VNode<'bump>],
new: &'bump [VNode<'bump>],
) {
if let Some(first_old) = old.get(0) {
self.remove_nodes(&old[1..], true);
self.stack
.create_children(new, MountType::Replace { old: first_old })
} else {
self.stack.create_children(new, MountType::Append {});
}
fn destroy_vomponent(&mut self, vc: &VComponent, gen_muts: bool) {
let scope_id = vc.associated_scope.get().unwrap();
let root = self.scopes.root_node(&scope_id);
self.remove_nodes(Some(root), gen_muts);
log::debug!("Destroying scope {:?}", scope_id);
self.scopes.try_remove(&scope_id).unwrap();
}
/// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &ScopeInner) {
let mut queue = scope.listeners.borrow_mut();
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) };
queue.push(long_listener as *const _)
fn attach_listener_to_scope(&mut self, listener: &'bump Listener<'bump>, scope: &Scope) {
let long_listener = unsafe { std::mem::transmute(listener) };
scope.items.borrow_mut().listeners.push(long_listener)
}
fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) {
if let Some(scope) = self
.stack
.current_scope()
.and_then(|id| self.vdom.get_scope_mut(id))
.and_then(|id| self.scopes.get_scope(&id))
{
// safety: this lifetime is managed by the logic on scope
let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) };
let extended = unsafe { std::mem::transmute(suspended) };
scope
.suspended_nodes
.items
.borrow_mut()
.insert(suspended.task_id, extended as *const _);
.suspended_nodes
.insert(suspended.task_id, extended);
}
}
}
-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>,
}
type StackHeapSize = [usize; 0];
type StackHeapSize = [usize; 16];
enum StackNodeStorage<'a, 'b> {
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> {
pub fn new<F>(val: F) -> Self
pub fn new<F>(_val: F) -> Self
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
{
unsafe {
let mut ptr: *const _ = &val as &dyn FnOnce(NodeFactory<'a>) -> VNode<'a>;
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(_val);
assert_eq!(
ptr as *const u8, &val as *const _ as *const u8,
"MISUSE: Closure returned different pointer"
);
assert_eq!(
std::mem::size_of_val(&*ptr),
std::mem::size_of::<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...)"
);
let val = move |fac: Option<NodeFactory<'a>>| {
let inner = slot.take().unwrap();
fac.map(inner)
};
// - 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>()
);
unsafe { LazyNodes::new_inner(val) }
}
let info = &words[1..];
let data = words[0] as *mut ();
let size = mem::size_of::<F>();
unsafe fn new_inner<F>(val: F) -> Self
where
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>() {
log::debug!("lazy nodes was too large to fit into stack. falling back to heap");
assert_eq!(
ptr as *const u8, &val as *const _ as *const u8,
"MISUSE: Closure returned different pointer"
);
assert_eq!(
std::mem::size_of_val(&*ptr),
std::mem::size_of::<F>(),
"MISUSE: Closure returned a subset pointer"
);
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
let words = ptr_as_slice(&mut ptr);
assert!(
words[0] == &val as *const _ as usize,
"BUG: Pointer layout is not (data_ptr, info...)"
);
// - Ensure that Self is aligned same as data requires
assert!(
std::mem::align_of::<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
// - Allows the data to be at the start for alignment purposes
{
let info_ofs = buf.as_ref().len() - info.len();
let info_dst = &mut buf.as_mut()[info_ofs..];
for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
*d = *v;
}
}
std::mem::forget(val);
let src_ptr = data as *const u8;
let dataptr = buf.as_mut()[..].as_mut_ptr() as *mut u8;
for i in 0..size {
*dataptr.add(i) = *src_ptr.add(i);
}
std::mem::forget(val);
Self {
inner: StackNodeStorage::Stack(LazyStack { _align: [], buf }),
}
Self {
inner: StackNodeStorage::Stack(LazyStack {
_align: [],
buf,
dropped: false,
}),
}
}
}
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
match self.inner {
StackNodeStorage::Heap(lazy) => lazy(f),
StackNodeStorage::Stack(stack) => stack.call(f),
StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(),
StackNodeStorage::Stack(mut stack) => stack.call(f),
}
}
}
@@ -117,34 +135,51 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
struct LazyStack {
_align: [u64; 0],
buf: StackHeapSize,
dropped: bool,
}
impl LazyStack {
unsafe fn create_boxed<'a>(&mut self) -> Box<dyn FnOnce(NodeFactory<'a>) -> VNode<'a>> {
fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size = mem::size_of::<*mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a>>()
/ mem::size_of::<usize>()
- 1;
let info_size =
mem::size_of::<*mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>>()
/ mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a> =
make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]);
let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
Box::from_raw(g)
}
self.dropped = true;
fn call(mut self, f: NodeFactory) -> VNode {
let boxed = unsafe { self.create_boxed() };
boxed(f)
let clos = unsafe { &mut *g };
clos(Some(f)).unwrap()
}
}
impl Drop for LazyStack {
fn drop(&mut self) {
let boxed = unsafe { self.create_boxed() };
mem::drop(boxed);
if !self.dropped {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size = mem::size_of::<
*mut dyn FnMut(Option<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() {
let bump = bumpalo::Bump::new();
simple_logger::init();
simple_logger::init().unwrap();
let factory = NodeFactory { bump: &bump };
@@ -191,3 +226,49 @@ fn it_works() {
dbg!(g);
}
#[test]
fn it_drops() {
use std::rc::Rc;
let bump = bumpalo::Bump::new();
simple_logger::init().unwrap();
// let factory = NodeFactory { scope: &bump };
struct DropInner {
id: i32,
}
impl Drop for DropInner {
fn drop(&mut self) {
log::debug!("dropping inner");
}
}
let val = Rc::new(10);
{
let it = (0..10)
.map(|i| {
let val = val.clone();
NodeFactory::annotate_lazy(move |f| {
log::debug!("hell closure");
let inner = DropInner { id: i };
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
})
})
.collect::<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
*/
pub mod bumpframe;
pub mod childiter;
pub mod component;
pub mod context;
pub mod diff;
pub mod diff_stack;
pub mod events;
pub mod heuristics;
pub mod hooklist;
pub mod hooks;
pub mod lazynodes;
pub mod mutations;
pub mod nodes;
pub mod resources;
pub mod scheduler;
pub mod scope;
pub mod tasks;
pub mod test_dom;
pub mod threadsafe;
pub mod util;
pub mod virtual_dom;
#[cfg(feature = "debug_vdom")]
pub mod debug_dom;
pub(crate) mod component;
pub(crate) mod diff;
pub(crate) mod lazynodes;
pub(crate) mod mutations;
pub(crate) mod nodes;
pub(crate) mod scope;
pub(crate) mod scopearena;
pub(crate) mod virtual_dom;
pub(crate) mod innerlude {
pub(crate) use crate::bumpframe::*;
pub(crate) use crate::childiter::*;
pub use crate::component::*;
pub use crate::context::*;
pub(crate) use crate::diff::*;
pub use crate::diff_stack::*;
pub use crate::events::*;
pub use crate::heuristics::*;
pub(crate) use crate::hooklist::*;
pub use crate::hooks::*;
pub use crate::diff::*;
pub use crate::lazynodes::*;
pub use crate::mutations::*;
pub use crate::nodes::*;
pub(crate) use crate::resources::*;
pub use crate::scheduler::*;
pub use crate::scope::*;
pub use crate::tasks::*;
pub use crate::test_dom::*;
pub use crate::threadsafe::*;
pub use crate::util::*;
pub use crate::scopearena::*;
pub use crate::virtual_dom::*;
pub type Element<'a> = Option<VNode<'a>>;
pub type FC<P> = for<'a> fn(Scope<'a, P>) -> Element<'a>;
pub type Element = Option<NodeLink>;
pub type FC<P> = for<'a> fn(Context<'a>, &'a P) -> Element;
}
pub use crate::innerlude::{
Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType,
Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, SuspendedContext,
TaskHandle, TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC,
Attribute, Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, IntoVNode,
LazyNodes, Listener, MountType, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeId,
UserEvent, VAnchor, VElement, VFragment, VNode, VSuspended, VirtualDom, FC,
};
pub mod prelude {
pub use crate::component::{fc_to_builder, Fragment, Properties, Scope};
pub use crate::context::Context;
pub use crate::hooks::*;
pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, ScopeChildren, FC};
pub use crate::component::{fc_to_builder, Fragment, Properties};
pub use crate::innerlude::Context;
pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, Scope, FC};
pub use crate::nodes::VNode;
pub use crate::VirtualDom;
}
+16 -5
View File
@@ -3,21 +3,32 @@
//! This module contains an internal API to generate these instructions.
use crate::innerlude::*;
use std::any::Any;
use std::{any::Any, fmt::Debug};
#[derive(Debug)]
pub struct Mutations<'a> {
pub edits: Vec<DomEdit<'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::*;
impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self {
let edits = Vec::new();
let noderefs = Vec::new();
Self { edits, noderefs }
Self {
edits: Vec::new(),
noderefs: Vec::new(),
effects: Vec::new(),
}
}
// Navigation
-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.
use crate::{
innerlude::{
empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner,
SuspendedContext,
},
innerlude::{Context, Element, Properties, Scope, ScopeId},
lazynodes::LazyNodes,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
any::Any,
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
};
@@ -19,8 +17,8 @@ use std::{
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
///
/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of:
///
/// - the [`rsx`] macro
/// - the [`html`] macro
/// - the [`NodeFactory`] API
pub enum VNode<'src> {
/// Text VNodes simply bump-allocated (or static) string slices
@@ -28,7 +26,8 @@ pub enum VNode<'src> {
/// # Example
///
/// ```
/// let node = cx.render(rsx!{ "hello" }).unwrap();
/// let mut vdom = VirtualDom::new();
/// let node = vdom.render_vnode(rsx!( "hello" ));
///
/// if let VNode::Text(vtext) = node {
/// assert_eq!(vtext.text, "hello");
@@ -43,7 +42,9 @@ pub enum VNode<'src> {
/// # Example
///
/// ```rust
/// let node = cx.render(rsx!{
/// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!{
/// div {
/// key: "a",
/// onclick: |e| log::info!("clicked"),
@@ -51,7 +52,8 @@ pub enum VNode<'src> {
/// style: { background_color: "red" }
/// "hello"
/// }
/// }).unwrap();
/// });
///
/// if let VNode::Element(velement) = node {
/// assert_eq!(velement.tag_name, "div");
/// assert_eq!(velement.namespace, None);
@@ -80,13 +82,13 @@ pub enum VNode<'src> {
/// # Example
///
/// ```rust
/// fn Example(cx: Context<()>) -> DomTree {
/// fn Example(cx: Context, props: &()) -> Element {
/// todo!()
/// }
///
/// let node = cx.render(rsx!{
/// Example {}
/// }).unwrap();
/// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!( Example {} ));
///
/// if let VNode::Component(vcomp) = node {
/// assert_eq!(vcomp.user_fc, Example as *const ());
@@ -96,13 +98,11 @@ pub enum VNode<'src> {
/// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed.
///
/// These nodes currently can only be constructed via the [`use_suspense`] hook.
///
/// # Example
///
/// ```rust
/// rsx!{
/// }
///
///
/// ```
Suspended(&'src VSuspended<'src>),
@@ -113,13 +113,32 @@ pub enum VNode<'src> {
/// # Example
///
/// ```rust
/// let node = cx.render(rsx! ( Fragment {} )).unwrap();
/// let mut vdom = VirtualDom::new();
///
/// let node = vdom.render_vnode(rsx!( Fragment {} ));
///
/// if let VNode::Fragment(frag) = node {
/// let root = &frag.children[0];
/// assert_eq!(root, VNode::Anchor);
/// }
/// ```
Anchor(&'src VAnchor),
/// A VNode that is actually a pointer to some nodes rather than the nodes directly. Useful when rendering portals
/// or eliding lifetimes on VNodes through runtime checks.
///
/// Linked VNodes can only be made through the [`Context::render`] method
///
/// Typically, linked nodes are found *not* in a VNode. When NodeLinks are in a VNode, the NodeLink was passed into
/// an `rsx!` call.
///
/// # Example
/// ```rust
/// let mut vdom = VirtualDom::new();
///
/// let node: NodeLink = vdom.render_vnode(rsx!( "hello" ));
/// ```
Linked(NodeLink),
}
impl<'src> VNode<'src> {
@@ -129,9 +148,11 @@ impl<'src> VNode<'src> {
VNode::Element(el) => el.key,
VNode::Component(c) => c.key,
VNode::Fragment(f) => f.key,
VNode::Text(_t) => None,
VNode::Suspended(_s) => None,
VNode::Anchor(_f) => None,
VNode::Linked(_c) => None,
}
}
@@ -151,11 +172,21 @@ impl<'src> VNode<'src> {
VNode::Element(el) => el.dom_id.get(),
VNode::Anchor(el) => el.dom_id.get(),
VNode::Suspended(el) => el.dom_id.get(),
VNode::Linked(_) => None,
VNode::Fragment(_) => None,
VNode::Component(_) => None,
}
}
pub(crate) fn children(&self) -> &[VNode<'src>] {
match &self {
VNode::Fragment(f) => f.children,
VNode::Component(_c) => todo!("children are not accessible through this"),
_ => &[],
}
}
// Create an "owned" version of the vnode.
pub fn decouple(&self) -> VNode<'src> {
match self {
@@ -167,7 +198,11 @@ impl<'src> VNode<'src> {
VNode::Fragment(f) => VNode::Fragment(VFragment {
children: f.children,
key: f.key,
is_static: f.is_static,
}),
VNode::Linked(c) => VNode::Linked(NodeLink {
scope_id: c.scope_id.clone(),
link_idx: c.link_idx.clone(),
node: c.node.clone(),
}),
}
}
@@ -177,21 +212,46 @@ impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self {
VNode::Element(el) => s
.debug_struct("VElement")
.debug_struct("VNode::VElement")
.field("name", &el.tag_name)
.field("key", &el.key)
.finish(),
VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNode::Anchor(_) => write!(s, "VAnchor"),
VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text),
VNode::Anchor(_) => write!(s, "VNode::VAnchor"),
VNode::Fragment(frag) => write!(s, "VFragment {{ children: {:?} }}", frag.children),
VNode::Suspended { .. } => write!(s, "VSuspended"),
VNode::Component(comp) => write!(s, "VComponent {{ fc: {:?}}}", comp.user_fc),
VNode::Fragment(frag) => {
write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
}
VNode::Suspended { .. } => write!(s, "VNode::VSuspended"),
VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc),
VNode::Linked(c) => write!(s, "VNode::VCached {{ scope_id: {:?} }}", c.scope_id.get()),
}
}
}
/// An Element's unique identifier.
///
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `ElementId` will be reused for a new component.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementId(pub usize);
impl std::fmt::Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl ElementId {
pub fn as_u64(self) -> u64 {
self.0 as u64
}
}
fn empty_cell() -> Cell<Option<ElementId>> {
Cell::new(None)
}
/// A placeholder node only generated when Fragments don't have any children.
pub struct VAnchor {
pub dom_id: Cell<Option<ElementId>>,
@@ -209,10 +269,7 @@ pub struct VText<'src> {
/// A list of VNodes with no single root.
pub struct VFragment<'src> {
pub key: Option<&'src str>,
pub children: &'src [VNode<'src>],
pub is_static: bool,
}
/// An element like a "div" with children, listeners, and attributes.
@@ -307,12 +364,11 @@ pub struct Listener<'bump> {
/// IE "click" - whatever the renderer needs to attach the listener by name.
pub event: &'static str,
#[allow(clippy::type_complexity)]
/// The actual callback that the user specified
pub(crate) callback:
RefCell<Option<BumpBox<'bump, dyn FnMut(Box<dyn std::any::Any + Send>) + 'bump>>>,
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(Box<dyn Any + Send>) + 'bump>>>,
}
pub type VCompCaller<'src> = BumpBox<'src, dyn Fn(Context) -> Element + 'src>;
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
pub struct VComponent<'src> {
@@ -320,21 +376,23 @@ pub struct VComponent<'src> {
pub associated_scope: Cell<Option<ScopeId>>,
pub is_static: bool,
// Function pointer to the FC that was used to generate this component
pub user_fc: *const (),
pub(crate) caller: &'src dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>,
pub(crate) can_memoize: bool,
pub(crate) hard_allocation: Cell<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) 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> {
@@ -342,7 +400,41 @@ pub struct VSuspended<'a> {
pub dom_id: Cell<Option<ElementId>>,
#[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.
@@ -361,7 +453,7 @@ impl<'a> NodeFactory<'a> {
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
self.bump
&self.bump
}
/// Directly pass in text blocks without the need to use the format_args macro.
@@ -381,7 +473,7 @@ impl<'a> NodeFactory<'a> {
Some(static_str) => (static_str, true),
None => {
use bumpalo::core_alloc::fmt::Write;
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
let mut str_buf = bumpalo::collections::String::new_in(self.bump);
str_buf.write_fmt(args).unwrap();
(str_buf.into_bump_str(), false)
}
@@ -437,18 +529,18 @@ impl<'a> NodeFactory<'a> {
A: 'a + AsRef<[Attribute<'a>]>,
V: 'a + AsRef<[VNode<'a>]>,
{
let listeners: &'a L = self.bump().alloc(listeners);
let listeners: &'a L = self.bump.alloc(listeners);
let listeners = listeners.as_ref();
let attributes: &'a A = self.bump().alloc(attributes);
let attributes: &'a A = self.bump.alloc(attributes);
let attributes = attributes.as_ref();
let children: &'a V = self.bump().alloc(children);
let children: &'a V = self.bump.alloc(children);
let children = children.as_ref();
let key = key.map(|f| self.raw_text(f).0);
VNode::Element(self.bump().alloc(VElement {
VNode::Element(self.bump.alloc(VElement {
tag_name,
key,
namespace,
@@ -479,19 +571,19 @@ impl<'a> NodeFactory<'a> {
pub fn component<P>(
&self,
component: fn(Scope<'a, P>) -> Element<'a>,
component: fn(Context<'a>, &'a P) -> Element,
props: P,
key: Option<Arguments>,
) -> VNode<'a>
where
P: Properties + 'a,
{
let bump = self.bump();
let bump = self.bump;
let props = bump.alloc(props);
let raw_props = props as *mut P as *mut ();
let bump_props = props as *mut P as *mut ();
let user_fc = component as *const ();
let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| {
let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| {
move |other: &VComponent| {
if user_fc == other.user_fc {
// Safety
@@ -500,12 +592,10 @@ impl<'a> NodeFactory<'a> {
// - Non-static P are autoderived to memoize as false
// - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe {
let real_other: &P = &*(other.raw_props as *const _ as *const P);
let real_other: &P = &*(other.bump_props as *const _ as *const P);
props.memoize(real_other)
};
log::debug!("comparing props...");
// It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait
props_memoized
@@ -513,7 +603,7 @@ impl<'a> NodeFactory<'a> {
false
}
}
}));
});
let drop_props = {
// create a closure to drop the props
@@ -521,9 +611,8 @@ impl<'a> NodeFactory<'a> {
let drop_props: &mut dyn FnMut() = bump.alloc_with(|| {
move || unsafe {
log::debug!("dropping props!");
if !has_dropped {
let real_other = raw_props as *mut _ as *mut P;
let real_other = bump_props as *mut _ as *mut P;
let b = BumpBox::from_raw(real_other);
std::mem::drop(b);
@@ -541,36 +630,45 @@ impl<'a> NodeFactory<'a> {
let key = key.map(|f| self.raw_text(f).0);
let caller: &'a mut dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> =
bump.alloc(move |scope: &ScopeInner| -> Element {
log::debug!("calling component renderr {:?}", scope.our_arena_idx);
let props: &'_ P = unsafe { &*(raw_props as *const P) };
let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) };
let s: Scope<'a, P> = (Context { scope: scp }, props);
let res: Element = component(s);
unsafe { std::mem::transmute(res) }
let caller: &'a mut dyn Fn(&'a Scope) -> Element =
bump.alloc(move |scope: &Scope| -> Element {
let props: &'_ P = unsafe { &*(bump_props as *const P) };
let res = component(scope, props);
res
});
let can_memoize = P::IS_STATIC;
VNode::Component(bump.alloc(VComponent {
user_fc,
comparator,
raw_props,
comparator: Some(comparator),
bump_props,
caller,
is_static: P::IS_STATIC,
key,
can_memoize: P::IS_STATIC,
can_memoize,
drop_props,
associated_scope: Cell::new(None),
hard_allocation: Cell::new(None),
}))
}
pub fn listener(
self,
event: &'static str,
callback: BumpBox<'a, dyn FnMut(Box<dyn Any + Send>) + 'a>,
) -> Listener<'a> {
Listener {
mounted_node: Cell::new(None),
event,
callback: RefCell::new(Some(callback)),
}
}
pub fn fragment_from_iter(
self,
node_iter: impl IntoIterator<Item = impl IntoVNode<'a>>,
) -> VNode<'a> {
let bump = self.bump();
let bump = self.bump;
let mut nodes = bumpalo::collections::Vec::new_in(bump);
for node in node_iter {
@@ -607,14 +705,64 @@ impl<'a> NodeFactory<'a> {
VNode::Fragment(VFragment {
children,
key: None,
is_static: false,
})
}
pub fn annotate_lazy<'z, 'b, F>(f: F) -> Option<LazyNodes<'z, 'b>>
where
F: FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b,
{
// this isn't quite feasible yet
// I think we need some form of interior mutability or state on nodefactory that stores which subtree was created
pub fn create_children(
self,
node_iter: impl IntoIterator<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))
}
}
@@ -687,7 +835,6 @@ impl<'a> IntoVNode<'a> for Option<LazyNodes<'a, '_>> {
Some(lazy) => lazy.call(cx),
None => VNode::Fragment(VFragment {
children: &[],
is_static: false,
key: None,
}),
}
@@ -712,79 +859,48 @@ impl IntoVNode<'_> for Arguments<'_> {
}
}
/// Access the children elements passed into the component
///
/// This enables patterns where a component is passed children from its parent.
///
/// ## Details
///
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
/// on the props that takes Context.
///
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
/// props are valid for the static lifetime.
///
/// ## Example
///
/// ```rust
/// const App: FC<()> = |(cx, props)|{
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
/// p {}
/// }
/// })
/// }
///
/// const CustomCard: FC<()> = |(cx, props)|{
/// cx.render(rsx!{
/// div {
/// h1 {"Title card"}
/// {props.children}
/// }
/// })
/// }
/// ```
///
/// ## Notes:
///
/// This method returns a "ScopeChildren" object. This object is copy-able and preserve the correct lifetime.
pub struct ScopeChildren<'a> {
root: Option<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>),
// called cx.render from a helper function
impl IntoVNode<'_> for Option<NodeLink> {
fn into_vnode(self, _cx: NodeFactory) -> VNode {
match self {
Some(node) => VNode::Linked(node),
None => {
todo!()
}
}
}
}
// essentially passing elements through props
// just build a new element in place
impl IntoVNode<'_> for &Option<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 futures_channel::mpsc::UnboundedSender;
use fxhash::FxHashMap;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
collections::HashMap,
future::Future,
pin::Pin,
rc::Rc,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
///
/// This lets components access props, schedule updates, integrate hooks, and expose shared state.
///
/// For the most part, the only method you should be using regularly is `render`.
///
/// ## Example
///
/// ```ignore
/// #[derive(Props)]
/// struct ExampleProps {
/// name: String
/// }
///
/// fn Example(cx: Context, props: &ExampleProps) -> Element {
/// cx.render(rsx!{ div {"Hello, {props.name}"} })
/// }
/// ```
pub type Context<'a> = &'a Scope;
/// Every component in Dioxus is represented by a `Scope`.
///
/// Scopes contain the state for hooks, the component's props, and other lifecycle information.
@@ -18,17 +42,28 @@ use std::{
///
/// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever
/// use case they might have.
pub struct ScopeInner {
// Book-keeping about our spot in the arena
pub(crate) parent_idx: Option<ScopeId>,
pub struct Scope {
// safety:
//
// pointers to scopes are *always* valid since they are bump allocated and never freed until this scope is also freed
// this is just a bit of a hack to not need an Rc to the ScopeArena.
// todo: replace this will ScopeId and provide a connection to scope arena directly
pub(crate) parent_scope: Option<*mut Scope>,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) subtree: Cell<u32>,
pub(crate) is_subtree_root: Cell<bool>,
// Nodes
pub(crate) frames: ActiveFrame,
pub(crate) caller: *const dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>,
pub(crate) generation: Cell<u32>,
// The double-buffering situation that we will use
pub(crate) frames: [BumpFrame; 2],
pub(crate) caller: *const dyn Fn(&Scope) -> Element,
/*
we care about:
@@ -36,9 +71,7 @@ pub struct ScopeInner {
- borrowed props (and how to drop them when the parent is dropped)
- suspended nodes (and how to call their callback when their associated tasks are complete)
*/
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) suspended_nodes: RefCell<FxHashMap<u64, *const VSuspended<'static>>>,
pub(crate) items: RefCell<SelfReferentialItems<'static>>,
// State
pub(crate) hooks: HookList,
@@ -46,34 +79,27 @@ pub struct ScopeInner {
// todo: move this into a centralized place - is more memory efficient
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
// whenever set_state is called, we fire off a message to the scheduler
// this closure _is_ the method called by schedule_update that marks this component as dirty
pub(crate) memoized_updater: Rc<dyn Fn()>,
pub(crate) shared: EventChannel,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
}
/// Public interface for Scopes.
impl ScopeInner {
/// Get the root VNode for this Scope.
///
/// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment.
///
/// # Example
/// ```rust
/// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
///
/// if let VNode::VElement(node) = base.root_node() {
/// assert_eq!(node.tag_name, "div");
/// }
/// ```
pub fn root_node(&self) -> &VNode {
self.frames.fin_head()
}
pub struct SelfReferentialItems<'a> {
pub(crate) listeners: Vec<&'a Listener<'a>>,
pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended<'a>>,
pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
}
/// A component's unique identifier.
///
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `ScopeId` will be reused for a new component.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ScopeId(pub usize);
// Public methods exposed to libraries and components
impl Scope {
/// Get the subtree ID that this scope belongs to.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
@@ -83,7 +109,7 @@ impl ScopeInner {
/// # Example
///
/// ```rust
/// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} }));
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
@@ -94,16 +120,6 @@ impl ScopeInner {
self.subtree.get()
}
pub(crate) fn new_subtree(&self) -> Option<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.
///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it.
@@ -111,7 +127,7 @@ impl ScopeInner {
/// # Example
///
/// ```rust
/// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} }));
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
@@ -131,7 +147,7 @@ impl ScopeInner {
/// # Example
///
/// ```rust
/// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} }));
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
@@ -139,7 +155,10 @@ impl ScopeInner {
/// assert_eq!(base.parent(), None);
/// ```
pub fn parent(&self) -> Option<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.
@@ -149,7 +168,7 @@ impl ScopeInner {
/// # Example
///
/// ```rust
/// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} }));
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
/// dom.rebuild();
/// let base = dom.base_scope();
///
@@ -158,116 +177,290 @@ impl ScopeInner {
pub fn scope_id(&self) -> ScopeId {
self.our_arena_idx
}
}
// The type of closure that wraps calling components
/// The type of task that gets sent to the task scheduler
/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes
pub type FiberTask = Pin<Box<dyn Future<Output = ScopeId>>>;
/// 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> {
// 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.
impl ScopeInner {
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
// we are going to break this lifetime by force in order to save it on ourselves.
// To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies.
// This should never happen, but is a good check to keep around
//
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
pub(crate) fn new(
caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>,
our_arena_idx: ScopeId,
parent_idx: Option<ScopeId>,
height: u32,
subtree: u32,
shared: EventChannel,
) -> Self {
let schedule_any_update = shared.schedule_any_immediate.clone();
/// Schedule an update for any component given its ScopeId.
///
/// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method.
///
/// This method should be used when you want to schedule an update for a component
pub fn schedule_update_any(&self) -> Rc<dyn Fn(ScopeId)> {
let chan = self.sender.clone();
Rc::new(move |id| {
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx));
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update(&self) {
self.needs_update_any(self.scope_id())
}
let caller = caller as *const _;
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn needs_update_any(&self, id: ScopeId) {
let _ = self.sender.unbounded_send(SchedulerMsg::Immediate(id));
}
// wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph
let caller = unsafe { std::mem::transmute(caller) };
/// Get the [`ScopeId`] of a mounted component.
///
/// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
pub fn bump(&self) -> &Bump {
&self.wip_frame().bump
}
Self {
memoized_updater,
shared,
caller,
parent_idx,
our_arena_idx,
height,
subtree: Cell::new(subtree),
is_subtree_root: Cell::new(false),
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
///
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
///
/// ## Example
///
/// ```ignore
/// fn Component(cx: Scope, props: &Props) -> Element {
/// // Lazy assemble the VNode tree
/// let lazy_nodes = rsx!("hello world");
///
/// // Actually build the tree and allocate it
/// cx.render(lazy_tree)
/// }
///```
pub fn render<'src>(&'src self, lazy_nodes: Option<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(),
hooks: Default::default(),
suspended_nodes: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
borrowed_props: Default::default(),
let node_ptr = node as *mut _;
let node_ptr = unsafe { std::mem::transmute(node_ptr) };
let link = NodeLink {
scope_id: Cell::new(Some(self.our_arena_idx)),
link_idx: Cell::new(0),
node: node_ptr,
};
Some(link)
}
/// Push an effect to be ran after the component has been successfully mounted to the dom
/// Returns the effect's position in the stack
pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
// this is some tricker to get around not being able to actually call fnonces
let mut slot = Some(effect);
let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
// wrap it in a type that will actually drop the contents
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
// erase the 'src lifetime for self-referential storage
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
let mut items = self.items.borrow_mut();
items.pending_effects.push(self_ref_fut);
items.pending_effects.len() - 1
}
/// Pushes the future onto the poll queue to be polled
/// The future is forcibly dropped if the component is not ready by the next render
pub fn push_task<'src>(&'src self, fut: impl Future<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(
&mut self,
caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>,
) {
log::debug!("Updating scope dependencies {:?}", self.our_arena_idx);
let caller = caller as *const _;
self.caller = unsafe { std::mem::transmute(caller) };
/// Create a new subtree with this scope as the root of the subtree.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// This method
///
/// # Example
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// todo!();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn create_subtree(&self) -> Option<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
/// causing UB in our tree.
/// Get the subtree ID that this scope belongs to.
///
/// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components
/// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove
/// any possible references to the data in the hook list.
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// References to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
/// # Example
///
/// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
/// be dropped.
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
// right now, we don't drop
self.borrowed_props
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
let scope_id = comp
.associated_scope
.get()
.expect("VComponents should be associated with a valid Scope");
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// let id = cx.get_current_subtree();
/// rsx!(cx, div { "Subtree {id}"})
/// };
/// ```
pub fn get_current_subtree(&self) -> u32 {
self.subtree()
}
if let Some(scope) = pool.get_scope_mut(scope_id) {
scope.ensure_drop_safety(pool);
/// Store a value between renders
///
/// This is *the* foundational hook for all other hooks.
///
/// - Initializer: closure used to create the initial hook state
/// - Runner: closure used to output a value every time the hook is used
///
/// To "cleanup" the hook, implement `Drop` on the stored hook value. Whenever the component is dropped, the hook
/// will be dropped as well.
///
/// # Example
///
/// ```ignore
/// // use_ref is the simplest way of storing a value between renders
/// fn use_ref<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();
drop_props();
}
});
const HOOK_ERR_MSG: &str = r###"
Unable to retrieve the hook that was initialized at this index.
Consult the `rules of hooks` to understand how to use hooks properly.
// Now that all the references are gone, we can safely drop our own references in our listeners.
self.listeners
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|listener| drop(listener.callback.borrow_mut().take()));
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Functions prefixed with "use" should never be called conditionally.
"###;
runner(self.hooks.next::<State>().expect(HOOK_ERR_MSG))
}
}
// Important internal methods
impl Scope {
/// The "work in progress frame" represents the frame that is currently being worked on.
pub(crate) fn wip_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
}
}
pub(crate) fn fin_frame(&self) -> &BumpFrame {
match self.generation.get() & 1 == 1 {
true => &self.frames[0],
false => &self.frames[1],
}
}
pub unsafe fn reset_wip_frame(&self) {
// todo: unsafecell or something
let bump = self.wip_frame() as *const _ as *mut Bump;
let g = &mut *bump;
g.reset();
}
pub fn cycle_frame(&self) {
self.generation.set(self.generation.get() + 1);
}
/// A safe wrapper around calling listeners
pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) {
let listners = self.listeners.borrow_mut();
pub(crate) fn call_listener(&self, event: UserEvent, element: ElementId) {
let listners = &mut self.items.borrow_mut().listeners;
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
let listener = listners.iter().find(|lis| {
let search = lis;
if search.event == event.name {
let search_id = search.mounted_node.get();
search_id.map(|f| f == element).unwrap_or(false)
@@ -276,8 +469,7 @@ impl ScopeInner {
}
});
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
if let Some(listener) = listener {
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event.event);
@@ -287,89 +479,137 @@ impl ScopeInner {
}
}
/*
General strategy here is to load up the appropriate suspended task and then run it.
Suspended nodes cannot be called repeatedly.
*/
// General strategy here is to load up the appropriate suspended task and then run it.
// Suspended nodes cannot be called repeatedly.
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
let mut nodes = self.suspended_nodes.borrow_mut();
let mut nodes = &mut self.items.get_mut().suspended_nodes;
if let Some(suspended) = nodes.remove(&task_id) {
let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
let cx: SuspendedContext<'a> = SuspendedContext {
inner: Context { scope: self },
};
let mut cb = sus.callback.borrow_mut().take().unwrap();
let new_node: Element<'a> = (cb)(cx);
let mut boxed = sus.callback.borrow_mut().take().unwrap();
let new_node: Element = boxed();
}
}
// run the list of effects
pub(crate) fn run_effects(&mut self, pool: &ResourcePool) {
todo!()
// let mut effects = self.frames.effects.borrow_mut();
// let mut effects = effects.drain(..).collect::<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(crate) fn run_effects(&mut self) {
for mut effect in self.items.get_mut().pending_effects.drain(..) {
effect();
}
}
pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
let node = *self.wip_frame().nodes.borrow().get(0).unwrap();
unsafe { std::mem::transmute(&*node) }
}
}
pub(crate) struct BumpFrame {
pub bump: Bump,
pub nodes: RefCell<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
//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
//!
//! In this file, multiple items are defined. This file is big, but should be documented well to
//! navigate the inner workings of the Dom. We try to keep these main mechanics in this file to limit
//! the possible exposed API surface (keep fields private). This particular implementation of VDOM
//! is extremely efficient, but relies on some unsafety under the hood to do things like manage
//! micro-heaps for components. We are currently working on refactoring the safety out into safe(r)
//! abstractions, but current tests (MIRI and otherwise) show no issues with the current implementation.
//!
//! Included is:
//! - The [`VirtualDom`] itself
//! - The [`Scope`] object for managing component lifecycle
//! - The [`ActiveFrame`] object for managing the Scope`s microheap
//! - The [`Context`] object for exposing VirtualDOM API to components
//! - The [`NodeFactory`] object for lazily exposing the `Context` API to the nodebuilder API
//!
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use crate::innerlude::*;
use std::{any::Any, rc::Rc};
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures_util::{Future, StreamExt};
use fxhash::FxHashSet;
use indexmap::IndexSet;
use std::pin::Pin;
use std::task::Poll;
use std::{any::Any, collections::VecDeque};
/// An integrated virtual node system that progresses events and diffs UI trees.
/// A virtual node system that progresses user events and diffs UI trees.
///
/// Differences are converted into patches which a renderer can use to draw the UI.
///
/// If you are building an App with Dioxus, you probably won't want to reach for this directly, instead opting to defer
/// to a particular crate's wrapper over the [`VirtualDom`] API.
/// ## Guide
///
/// Components are defined as simple functions that take [`Context`] and a [`Properties`] type and return an [`Element`].
///
/// Example
/// ```rust
/// static App: FC<()> = |(cx, props)|{
/// #[derive(Props, PartialEq)]
/// struct AppProps {
/// title: String
/// }
///
/// fn App(cx: Context, props: &AppProps) -> Element {
/// cx.render(rsx!(
/// div {"hello, {props.title}"}
/// ))
/// }
/// ```
///
/// Components may be composed to make complex apps.
///
/// ```rust
/// fn App(cx: Context, props: &AppProps) -> Element {
/// cx.render(rsx!(
/// NavBar { routes: ROUTES }
/// Title { "{props.title}" }
/// Footer {}
/// ))
/// }
/// ```
///
/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
/// draw the UI.
///
/// ```rust
/// let mut vdom = VirtualDom::new(App);
/// let edits = vdom.rebuild();
/// ```
///
/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler.
///
/// ```rust
/// let channel = vdom.get_scheduler_channel();
/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent {
/// // ...
/// }))
/// ```
///
/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
///
/// ```rust
/// vdom.wait_for_work().await;
/// ```
///
/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and
/// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be
/// handled by the renderer.
///
/// ```rust
/// let mutations = vdom.work_with_deadline(|| false);
/// for edit in mutations {
/// apply(edit);
/// }
/// ```
///
/// ## Building an event loop around Dioxus:
///
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
///
/// ```rust
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!{
/// div {
/// "Hello World"
/// }
/// div { "Hello World" }
/// })
/// }
///
/// async fn main() {
/// let mut dom = VirtualDom::new(App);
///
/// let mut inital_edits = dom.rebuild();
/// initialize_screen(inital_edits);
/// apply_edits(inital_edits);
///
/// loop {
/// let next_frame = TimeoutFuture::new(Duration::from_millis(16));
/// let edits = dom.run_with_deadline(next_frame).await;
/// dom.wait_for_work().await;
/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16));
/// let deadline = || (&mut frame_timeout).now_or_never();
/// let edits = dom.run_with_deadline(deadline).await;
/// apply_edits(edits);
/// render_frame();
/// }
/// }
/// ```
pub struct VirtualDom {
scheduler: Scheduler,
base_scope: ScopeId,
root_fc: Box<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
_root_caller: Rc<dyn Any>,
receiver: UnboundedReceiver<SchedulerMsg>,
sender: UnboundedSender<SchedulerMsg>,
pending_futures: FxHashSet<ScopeId>,
pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>,
}
// Methods to create the VirtualDom
impl VirtualDom {
/// Create a new VirtualDOM with a component that does not have special props.
///
@@ -80,7 +135,7 @@ impl VirtualDom {
///
/// # Example
/// ```
/// fn Example(cx: Context<()>) -> DomTree {
/// fn Example(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div { "hello world" } ))
/// }
///
@@ -109,7 +164,7 @@ impl VirtualDom {
/// name: &'static str
/// }
///
/// fn Example(cx: Context<SomeProps>) -> DomTree {
/// fn Example(cx: Context, props: &SomeProps) -> Element {
/// cx.render(rsx!{ div{ "hello {cx.name}" } })
/// }
///
@@ -122,12 +177,12 @@ impl VirtualDom {
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<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>();
Self::new_with_props_and_scheduler(root, root_props, sender, receiver)
}
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler.
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler
///
/// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the
/// VirtualDom to be created just to retrieve its channel receiver.
@@ -137,38 +192,26 @@ impl VirtualDom {
sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>,
) -> 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 root_caller: Rc<dyn Fn(&ScopeInner) -> Element> = Rc::new(move |scope: &ScopeInner| {
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(),
)
});
let pending_messages = VecDeque::new();
let mut dirty_scopes = IndexSet::new();
dirty_scopes.insert(base_scope);
Self {
_root_caller: Rc::new(root_caller),
root_fc,
scopes: Box::new(scopes),
base_scope,
scheduler,
root_props,
receiver,
// todo: clean this up manually?
_root_caller: caller_ref,
pending_messages,
pending_futures: Default::default(),
dirty_scopes,
sender,
}
}
@@ -176,134 +219,117 @@ impl VirtualDom {
///
/// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
/// directly.
pub fn base_scope(&self) -> &ScopeInner {
self.scheduler.pool.get_scope(self.base_scope).unwrap()
///
/// # Example
pub fn base_scope(&self) -> &Scope {
self.get_scope(&self.base_scope).unwrap()
}
/// Get the [`Scope`] for a component given its [`ScopeId`]
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeInner> {
self.scheduler.pool.get_scope(id)
}
/// Update the root props of this VirtualDOM.
///
/// This method returns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately,
/// so calling this method will block the main thread until computation is done.
///
/// ## Example
///
/// ```rust
/// #[derive(Props, PartialEq)]
/// struct AppProps {
/// route: &'static str
/// }
/// static App: FC<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
/// ```
/// static App: FC<()> = |(cx, props)|cx.render(rsx!{ "hello world" });
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
///
/// apply_edits(edits);
/// ```
pub fn rebuild(&mut self) -> Mutations {
self.scheduler.rebuild(self.base_scope)
///
///
pub fn get_scope<'a>(&'a self, id: &ScopeId) -> Option<&'a Scope> {
self.scopes.get_scope(id)
}
/// Compute a manual diff of the VirtualDOM between states.
///
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propagated as an event.
///
/// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used
/// to force an update of the DOM when the state of the app is changed outside of the app.
///
/// Get an [`UnboundedSender`] handle to the channel used by the scheduler.
///
/// # Example
///
/// ```rust
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
/// }
///
/// static App: FC<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 {
self.scheduler.hard_diff(self.base_scope)
pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
self.sender.clone()
}
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
/// Check if the [`VirtualDom`] has any pending updates or work to be done.
///
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
pub fn run_immediate(&mut self) -> Option<Vec<Mutations>> {
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
/// # Example
///
/// ```rust
///
///
/// ```
pub fn has_any_work(&self) -> bool {
!(self.dirty_scopes.is_empty() && self.pending_messages.is_empty())
}
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
// todo: poll the events once even if there is work to do to prevent starvation
// if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed
if self.pending_futures.is_empty() {
self.pending_messages
.push_front(self.receiver.next().await.unwrap());
} else {
None
struct PollTasks<'a> {
pending_futures: &'a FxHashSet<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
///
/// ```no_run
/// static App: FC<()> = |(cx, props)|rsx!(cx, div {"hello"} );
/// fn App(cx: Context, props: &()) -> Element {
/// cx.render(rsx!( div {"hello"} ))
/// }
///
/// let mut dom = VirtualDom::new(App);
///
/// loop {
/// let deadline = TimeoutFuture::from_ms(16);
/// let mut timeout = TimeoutFuture::from_ms(16);
/// let deadline = move || (&mut timeout).now_or_never();
///
/// let mutations = dom.run_with_deadline(deadline).await;
///
/// apply_mutations(mutations);
/// }
/// ```
@@ -350,48 +383,312 @@ impl VirtualDom {
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec<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> {
self.scheduler.pool.channel.sender.clone()
}
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
// todo: poll the events once even if there is work to do to prevent starvation
if self.scheduler.has_any_work() {
return;
}
use futures_util::StreamExt;
// Wait for any new events if we have nothing to do
let tasks_fut = self.scheduler.async_tasks.next();
let scheduler_fut = self.scheduler.receiver.next();
use futures_util::future::{select, Either};
match select(tasks_fut, scheduler_fut).await {
// poll the internal futures
Either::Left((_id, _)) => {
//
while self.has_any_work() {
while let Ok(Some(msg)) = self.receiver.try_next() {
self.pending_messages.push_front(msg);
}
// wait for an external event
Either::Right((msg, _)) => match msg.unwrap() {
SchedulerMsg::Task(t) => {
self.scheduler.handle_task(t);
while let Some(msg) = self.pending_messages.pop_back() {
match msg {
SchedulerMsg::Immediate(id) => {
self.dirty_scopes.insert(id);
}
SchedulerMsg::UiEvent(event) => {
if let Some(element) = event.mounted_dom_id {
log::info!("Calling listener {:?}, {:?}", event.scope_id, element);
if let Some(scope) = self.scopes.get_scope(&event.scope_id) {
// TODO: bubble properly here
scope.call_listener(event, element);
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
self.pending_messages.push_front(dirty_scope);
}
}
} else {
log::debug!("User event without a targetted ElementId. Not currently supported.\nUnsure how to proceed. {:?}", event);
}
}
}
SchedulerMsg::Immediate(im) => {
self.scheduler.dirty_scopes.insert(im);
}
let scopes = &self.scopes;
let mut diff_state = DiffState::new(scopes);
let mut ran_scopes = FxHashSet::default();
// todo: the 2021 version of rust will let us not have to force the borrow
// let scopes = &self.scopes;
// Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height
self.dirty_scopes
.retain(|id| scopes.get_scope(id).is_some());
self.dirty_scopes.sort_by(|a, b| {
let h1 = scopes.get_scope(a).unwrap().height;
let h2 = scopes.get_scope(b).unwrap().height;
h1.cmp(&h2).reverse()
});
if let Some(scopeid) = self.dirty_scopes.pop() {
log::info!("handling dirty scope {:?}", scopeid);
if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
if self.scopes.run_scope(&scopeid) {
let (old, new) = (
self.scopes.wip_head(&scopeid),
self.scopes.fin_head(&scopeid),
);
diff_state.stack.scope_stack.push(scopeid);
diff_state.stack.push(DiffInstruction::Diff { new, old });
}
}
SchedulerMsg::UiEvent(evt) => {
self.scheduler.ui_events.push_back(evt);
}
let work_completed = diff_state.work(&mut deadline);
if work_completed {
let DiffState {
mutations,
seen_scopes,
stack,
..
} = diff_state;
for scope in seen_scopes {
self.dirty_scopes.remove(&scope);
}
},
// // I think the stack should be empty at the end of diffing?
// debug_assert_eq!(stack.scope_stack.len(), 1);
committed_mutations.push(mutations);
} else {
// leave the work in an incomplete state
log::debug!("don't have a mechanism to pause work (yet)");
return committed_mutations;
}
}
committed_mutations
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch
///
/// The diff machine expects the RealDom's stack to be the root of the application.
///
/// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
/// root component will be ran once and then diffed. All updates will flow out as mutations.
///
/// All state stored in components will be completely wiped away.
///
/// # Example
/// ```
/// static App: FC<()> = |cx, props| cx.render(rsx!{ "hello world" });
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
///
/// apply_edits(edits);
/// ```
pub fn rebuild(&mut self) -> Mutations {
let mut diff_state = DiffState::new(&self.scopes);
let scope_id = self.base_scope;
if self.scopes.run_scope(&scope_id) {
diff_state
.stack
.create_node(self.scopes.fin_head(&scope_id), MountType::Append);
diff_state.stack.scope_stack.push(scope_id);
diff_state.work(|| false);
}
diff_state.mutations
}
/// Compute a manual diff of the VirtualDOM between states.
///
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propagated as an event.
///
/// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used
/// to force an update of the DOM when the state of the app is changed outside of the app.
///
///
/// # Example
/// ```rust
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
/// }
///
/// static App: FC<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);
}
fn Parent((cx, _): Scope<()>) -> Element {
let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {});
fn Parent(cx: Context, props: &()) -> Element {
let value = cx.use_hook(|_| String::new(), |f| &*f);
cx.render(rsx! {
div {
@@ -28,7 +28,7 @@ struct ChildProps<'a> {
name: &'a str,
}
fn Child<'a>((cx, props): Scope<'a, ChildProps>) -> Element<'a> {
fn Child(cx: Context, props: &ChildProps) -> Element {
cx.render(rsx! {
div {
h1 { "it's nested" }
@@ -42,7 +42,7 @@ struct Grandchild<'a> {
name: &'a str,
}
fn Child2<'a>((cx, props): Scope<'a, Grandchild>) -> Element<'a> {
fn Child2(cx: Context, props: &Grandchild) -> Element {
cx.render(rsx! {
div { "Hello {props.name}!" }
})
+14 -31
View File
@@ -21,7 +21,7 @@ fn new_dom<P: 'static + Send>(app: FC<P>, props: P) -> VirtualDom {
#[test]
fn test_original_diff() {
static APP: FC<()> = |(cx, props)| {
static APP: FC<()> = |cx, props| {
cx.render(rsx! {
div {
div {
@@ -57,17 +57,17 @@ fn test_original_diff() {
#[test]
fn create() {
static APP: FC<()> = |(cx, props)| {
static APP: FC<()> = |cx, props| {
cx.render(rsx! {
div {
div {
"Hello, world!"
div {
div {
// Fragment {
// "hello"
// "world"
// }
Fragment {
"hello"
"world"
}
}
}
}
@@ -120,7 +120,7 @@ fn create() {
#[test]
fn create_list() {
static APP: FC<()> = |(cx, props)| {
static APP: FC<()> = |cx, props| {
cx.render(rsx! {
{(0..3).map(|f| rsx!{ div {
"hello"
@@ -169,7 +169,7 @@ fn create_list() {
#[test]
fn create_simple() {
static APP: FC<()> = |(cx, props)| {
static APP: FC<()> = |cx, props| {
cx.render(rsx! {
div {}
div {}
@@ -207,7 +207,7 @@ fn create_simple() {
}
#[test]
fn create_components() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx! {
Child { "abc1" }
Child { "abc2" }
@@ -215,12 +215,12 @@ fn create_components() {
})
};
#[derive(Props)]
struct ChildProps<'a> {
children: ScopeChildren<'a>,
#[derive(Props, PartialEq)]
struct ChildProps {
children: Element,
}
fn Child<'a>((cx, props): Scope<'a, ChildProps<'a>>) -> Element {
fn Child(cx: Context, props: &ChildProps) -> Element {
cx.render(rsx! {
h1 {}
div { {&props.children} }
@@ -273,7 +273,7 @@ fn create_components() {
}
#[test]
fn anchors() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx! {
{true.then(|| rsx!{ div { "hello" } })}
{false.then(|| rsx!{ div { "goodbye" } })}
@@ -299,20 +299,3 @@ fn anchors() {
]
);
}
#[test]
fn suspended() {
static App: FC<()> = |(cx, props)| {
let val = use_suspense(cx, || async {}, |cx, p| todo!());
cx.render(rsx! { {val} })
};
let mut dom = new_dom(App, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 },]
);
}
+28 -47
View File
@@ -5,17 +5,17 @@
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom};
use dioxus::{prelude::*, DomEdit, VSuspended};
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
mod test_logging;
fn new_dom() -> TestDom {
fn new_dom() -> VirtualDom {
const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED);
TestDom::new()
VirtualDom::new(|cx, props| todo!())
}
use DomEdit::*;
@@ -24,7 +24,7 @@ use DomEdit::*;
#[test]
fn html_and_rsx_generate_the_same_output() {
let dom = new_dom();
let (create, change) = dom.lazy_diff(
let (create, change) = dom.diff_lazynodes(
rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ),
);
@@ -58,7 +58,7 @@ fn html_and_rsx_generate_the_same_output() {
fn fragments_create_properly() {
let dom = new_dom();
let create = dom.create(rsx! {
let create = dom.create_vnodes(rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
@@ -107,7 +107,7 @@ fn empty_fragments_create_anchors() {
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) });
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
@@ -133,7 +133,7 @@ fn empty_fragments_create_many_anchors() {
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) });
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
@@ -179,7 +179,7 @@ fn empty_fragments_create_anchors_with_many_children() {
})
});
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
@@ -231,7 +231,7 @@ fn many_items_become_fragment() {
});
let right = rsx!({ (0..0).map(|_| rsx! { div {} }) });
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
@@ -257,12 +257,12 @@ fn many_items_become_fragment() {
]
);
// hmmmmmmmmm worried about reusing IDs that we shouldnt be
// note: the ID gets reused
assert_eq!(
change.edits,
[
Remove { root: 2 },
CreatePlaceholder { root: 4 },
CreatePlaceholder { root: 3 },
ReplaceWith { root: 0, m: 1 },
]
);
@@ -284,7 +284,7 @@ fn two_equal_fragments_are_equal() {
})
});
let (_create, change) = dom.lazy_diff(left, right);
let (_create, change) = dom.diff_lazynodes(left, right);
assert!(change.edits.is_empty());
}
@@ -302,7 +302,7 @@ fn two_fragments_with_differrent_elements_are_differet() {
p {}
);
let (_create, changes) = dom.lazy_diff(left, right);
let (_create, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
@@ -335,7 +335,7 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
p {}
);
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
@@ -391,7 +391,7 @@ fn two_fragments_with_same_elements_are_differet() {
p {}
);
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
create.edits,
[
@@ -433,7 +433,7 @@ fn keyed_diffing_order() {
let dom = new_dom();
let left = rsx!(
// {(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
p {"e"}
);
let right = rsx!(
@@ -441,7 +441,7 @@ fn keyed_diffing_order() {
p {"e"}
);
let (create, change) = dom.lazy_diff(left, right);
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[Remove { root: 2 }, Remove { root: 3 }, Remove { root: 4 },]
@@ -465,7 +465,7 @@ fn keyed_diffing_out_of_order() {
})
});
let (_, changes) = dom.lazy_diff(left, right);
let (_, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:?}", &changes);
assert_eq!(
changes.edits,
@@ -490,7 +490,7 @@ fn keyed_diffing_out_of_order_adds() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
@@ -517,7 +517,7 @@ fn keyed_diffing_out_of_order_adds_2() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
@@ -545,7 +545,7 @@ fn keyed_diffing_out_of_order_adds_3() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
@@ -573,7 +573,7 @@ fn keyed_diffing_out_of_order_adds_4() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
@@ -601,7 +601,7 @@ fn keyed_diffing_out_of_order_adds_5() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[PushRoot { root: 4 }, InsertBefore { n: 1, root: 3 }]
@@ -624,7 +624,7 @@ fn keyed_diffing_additions() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
assert_eq!(
change.edits,
[
@@ -657,7 +657,7 @@ fn keyed_diffing_additions_and_moves_on_ends() {
})
});
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
log::debug!("{:?}", change);
assert_eq!(
change.edits,
@@ -696,7 +696,7 @@ fn keyed_diffing_additions_and_moves_in_middle() {
});
// LIS: 4, 5, 6
let (_, change) = dom.lazy_diff(left, right);
let (_, change) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", change);
assert_eq!(
change.edits,
@@ -745,7 +745,7 @@ fn controlled_keyed_diffing_out_of_order() {
});
// LIS: 5, 6
let (_, changes) = dom.lazy_diff(left, right);
let (_, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
@@ -787,7 +787,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
})
});
let (_, changes) = dom.lazy_diff(left, right);
let (_, changes) = dom.diff_lazynodes(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
@@ -802,22 +802,3 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
]
);
}
#[test]
fn suspense() {
let dom = new_dom();
todo!()
// let edits = dom.create(Some(LazyNodes::new(|f| {
// use std::cell::{Cell, RefCell};
// VNode::Suspended(f.bump().alloc(VSuspended {
// task_id: 0,
// callback: RefCell::new(None),
// dom_id: Cell::new(None),
// }))
// })));
// assert_eq!(
// edits.edits,
// [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 }]
// );
}
-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.
use dioxus::prelude::*;
use dioxus_core as dioxus;
use dioxus_core::DomEdit::*;
use dioxus_core::ScopeId;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
use std::sync::{Arc, Mutex};
@@ -20,7 +24,7 @@ fn manual_diffing() {
value: Shared<&'static str>,
}
static App: FC<AppProps> = |(cx, props)| {
static App: FC<AppProps> = |cx, props| {
let val = props.value.lock().unwrap();
cx.render(rsx! { div { "{val}" } })
};
@@ -37,7 +41,275 @@ fn manual_diffing() {
*value.lock().unwrap() = "goodbye";
let edits = dom.diff();
let edits = dom.rebuild();
log::debug!("edits: {:?}", edits);
}
#[test]
fn events_generate() {
static App: FC<()> = |cx, _| {
let mut count = use_state(cx, || 0);
let inner = match *count {
0 => {
rsx! {
div {
onclick: move |_| count += 1,
div {
"nested"
}
"Click me!"
}
}
}
_ => todo!(),
};
cx.render(inner)
};
let mut dom = VirtualDom::new(App);
let mut channel = dom.get_scheduler_channel();
assert!(dom.has_any_work());
let edits = dom.work_with_deadline(|| false);
assert_eq!(
edits[0].edits,
[
CreateElement {
tag: "div",
root: 0,
},
NewEventListener {
event_name: "click",
scope: ScopeId(0),
root: 0,
},
CreateElement {
tag: "div",
root: 1,
},
CreateTextNode {
text: "nested",
root: 2,
},
AppendChildren { many: 1 },
CreateTextNode {
text: "Click me!",
root: 3,
},
AppendChildren { many: 2 },
]
)
}
#[test]
fn components_generate() {
static App: FC<()> = |cx, _| {
let mut render_phase = use_state(cx, || 0);
render_phase += 1;
cx.render(match *render_phase {
0 => rsx!("Text0"),
1 => rsx!(div {}),
2 => rsx!("Text2"),
3 => rsx!(Child {}),
4 => rsx!({ None as Option<()> }),
5 => rsx!("text 3"),
6 => rsx!({ (0..2).map(|f| rsx!("text {f}")) }),
7 => rsx!(Child {}),
_ => todo!(),
})
};
static Child: FC<()> = |cx, _| {
println!("running child");
cx.render(rsx! {
h1 {}
})
};
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "Text0",
root: 0,
},
AppendChildren { many: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateElement {
tag: "div",
root: 1,
},
ReplaceWith { root: 0, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "Text2",
root: 2,
},
ReplaceWith { root: 1, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateElement { tag: "h1", root: 3 },
ReplaceWith { root: 2, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 4 }, ReplaceWith { root: 3, m: 1 },]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "text 3",
root: 5,
},
ReplaceWith { root: 4, m: 1 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateTextNode {
text: "text 0",
root: 6,
},
CreateTextNode {
text: "text 1",
root: 7,
},
ReplaceWith { root: 5, m: 2 },
]
);
let edits = dom.hard_diff(&ScopeId(0)).unwrap();
assert_eq!(
edits.edits,
[
CreateElement { tag: "h1", root: 8 },
ReplaceWith { root: 6, m: 1 },
Remove { root: 7 },
]
);
}
#[test]
fn component_swap() {
// simple_logger::init();
static App: FC<()> = |cx, _| {
let mut render_phase = use_state(cx, || 0);
render_phase += 1;
cx.render(match *render_phase {
0 => rsx!(
div {
NavBar {}
Dashboard {}
}
),
1 => rsx!(
div {
NavBar {}
Results {}
}
),
2 => rsx!(
div {
NavBar {}
Dashboard {}
}
),
3 => rsx!(
div {
NavBar {}
Results {}
}
),
4 => rsx!(
div {
NavBar {}
Dashboard {}
}
),
_ => rsx!("blah"),
})
};
static NavBar: FC<()> = |cx, _| {
println!("running navbar");
cx.render(rsx! {
h1 {
"NavBar"
{(0..3).map(|f| rsx!(NavLink {}))}
}
})
};
static NavLink: FC<()> = |cx, _| {
println!("running navlink");
cx.render(rsx! {
h1 {
"NavLink"
}
})
};
static Dashboard: FC<()> = |cx, _| {
println!("running dashboard");
cx.render(rsx! {
div {
"dashboard"
}
})
};
static Results: FC<()> = |cx, _| {
println!("running results");
cx.render(rsx! {
div {
"results"
}
})
};
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild();
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
let edits = dom.work_with_deadline(|| false);
dbg!(&edits);
}
+3 -3
View File
@@ -1,6 +1,6 @@
#![allow(unused, non_upper_case_globals)]
use dioxus::{prelude::*, DomEdit, Mutations, TestDom};
use dioxus::{prelude::*, DomEdit, Mutations};
use dioxus_core as dioxus;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
@@ -13,12 +13,12 @@ mod test_logging;
fn shared_state_test() {
struct MySharedState(&'static str);
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.provide_state(MySharedState("world!"));
cx.render(rsx!(Child {}))
};
static Child: FC<()> = |(cx, props)| {
static Child: FC<()> = |cx, props| {
let shared = cx.consume_state::<MySharedState>()?;
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]
fn app_runs() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
//
cx.render(rsx!( div{"hello"} ))
};
@@ -28,7 +28,7 @@ fn app_runs() {
#[test]
fn fragments_work() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
div{"hello"}
div{"goodbye"}
@@ -42,7 +42,7 @@ fn fragments_work() {
#[test]
fn lists_work() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
h1 {"hello"}
{(0..6).map(|f| rsx!(span{ "{f}" }))}
@@ -55,7 +55,7 @@ fn lists_work() {
#[test]
fn conditional_rendering() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
h1 {"hello"}
{true.then(|| rsx!(span{ "a" }))}
@@ -72,13 +72,13 @@ fn conditional_rendering() {
#[test]
fn child_components() {
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx!(
{true.then(|| rsx!(Child { }))}
{false.then(|| rsx!(Child { }))}
))
};
static Child: FC<()> = |(cx, props)| {
static Child: FC<()> = |cx, props| {
cx.render(rsx!(
h1 {"hello"}
h1 {"goodbye"}
@@ -91,13 +91,14 @@ fn child_components() {
#[test]
fn suspended_works() {
static App: FC<()> = |(cx, props)| {
let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!());
// let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"});
cx.render(rsx!("hello" { title }))
};
todo!()
// static App: FC<()> = |cx, props| {
// let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!());
// // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"});
// cx.render(rsx!("hello" { title }))
// };
let mut vdom = VirtualDom::new(App);
let edits = vdom.rebuild();
dbg!(edits);
// let mut vdom = VirtualDom::new(App);
// let edits = vdom.rebuild();
// dbg!(edits);
}
+1 -1
View File
@@ -24,6 +24,7 @@ tokio = { version = "1.12.0", features = [
"rt",
], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro" }
dioxus-html = { path = "../html", features = ["serialize"] }
[features]
default = ["tokio_runtime"]
@@ -31,6 +32,5 @@ tokio_runtime = ["tokio"]
[dev-dependencies]
dioxus-html = { path = "../html" }
dioxus-hooks = { path = "../hooks" }
simple_logger = "1.13.0"
+3 -7
View File
@@ -1,7 +1,6 @@
use std::cell::RefCell;
use dioxus::prelude::Scope;
use dioxus::ScopeChildren;
use dioxus_core as dioxus;
use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties};
use dioxus_core_macro::Props;
@@ -44,7 +43,7 @@ pub struct WebviewWindowProps<'a> {
/// focuse me
onfocused: &'a dyn FnMut(()),
children: ScopeChildren<'a>,
children: Element,
}
/// A handle to a
@@ -57,7 +56,7 @@ pub struct WebviewWindowProps<'a> {
///
///
///
pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element<'a> {
pub fn WebviewWindow(cx: Context, props: &WebviewWindowProps) -> Element {
let dtcx = cx.consume_state::<RefCell<DesktopContext>>()?;
cx.use_hook(
@@ -67,9 +66,6 @@ pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element<
|state| {
//
},
|hook| {
//
},
);
// render the children directly
@@ -94,7 +90,7 @@ fn syntax_works() {
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
static App: FC<()> = |(cx, props)| {
static App: FC<()> = |cx, props| {
cx.render(rsx! {
// left window
WebviewWindow {
+1
View File
@@ -0,0 +1 @@
+6 -6
View File
@@ -4,7 +4,8 @@
use std::sync::Arc;
use std::{any::Any, rc::Rc};
use dioxus_core::{events::on::MouseEvent, ElementId, EventPriority, ScopeId, UserEvent};
use dioxus_core::{ElementId, EventPriority, ScopeId, UserEvent};
use dioxus_html::on::*;
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
@@ -23,7 +24,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
contents,
} = ims.into_iter().next().unwrap();
let scope = ScopeId(scope as usize);
let scope_id = ScopeId(scope as usize);
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let name = event_name_from_typ(&event);
@@ -31,15 +32,14 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
UserEvent {
name,
event,
scope,
priority: EventPriority::Low,
scope_id,
mounted_dom_id,
event,
}
}
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box<dyn Any + Send> {
use dioxus_core::events::on::*;
match name {
"copy" | "cut" | "paste" => {
//
+3 -5
View File
@@ -13,7 +13,6 @@ use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use cfg::DesktopConfig;
use dioxus_core::scheduler::SchedulerMsg;
use dioxus_core::*;
use serde::{Deserialize, Serialize};
@@ -167,7 +166,7 @@ pub fn run<T: 'static + Send + Sync>(
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
if let Some(BridgeEvent::Update(edits)) = rx.blocking_recv() {
log::info!("bridge received message {:?}", edits);
log::info!("bridge received message");
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
} else {
log::info!("none received message");
@@ -277,7 +276,7 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
runtime.block_on(async move {
let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
let _ = vir.get_event_sender();
let _ = vir.get_scheduler_channel();
let edits = vir.rebuild();
@@ -299,14 +298,13 @@ pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
// todo: maybe we want to schedule ourselves in
// on average though, the virtualdom running natively is stupid fast
let mut muts = vir.run_with_deadline(|| false);
let mut muts = vir.work_with_deadline(|| false);
log::debug!("finished running with deadline");
let mut edits = vec![];
while let Some(edit) = muts.pop() {
log::debug!("sending message on channel with edit {:?}", edit);
let edit_string = serde_json::to_value(Evt { edits: edit.edits })
.expect("serializing edits should never fail");
edits.push(edit_string);
+11 -8
View File
@@ -17,6 +17,8 @@ pub(crate) struct ProvidedStateInner<T> {
impl<T> ProvidedStateInner<T> {
pub(crate) fn notify_consumers(&mut self) {
for consumer in self.consumers.iter() {
println!("notifiying {:?}", consumer);
// log::debug("notifiying {:?}", consumer);
(self.notify_any)(*consumer);
}
}
@@ -82,13 +84,6 @@ pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option<UseSharedStat
_ => 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,
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(crate) cx: Context<'a>,
@@ -172,6 +176,5 @@ pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T)
cx.provide_state(state)
},
|inner| {},
|_| {},
)
}
-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);
UseRef { inner }
},
|_| {},
)
}
-1
View File
@@ -69,7 +69,6 @@ pub fn use_state<'a, T: 'static>(
UseState { inner: &*hook }
},
|_| {},
)
}
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
[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