diff --git a/examples/borrowed.rs b/examples/borrowed.rs index dc4805500..786de9da9 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -21,7 +21,7 @@ fn main() { } fn App((cx, props): Scope<()>) -> Element { - let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); + let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); let first = text.get_mut(0).unwrap(); diff --git a/examples/crm.rs b/examples/crm.rs index be1abb3d9..e0c873221 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -90,15 +90,17 @@ static App: FC<()> = |(cx, _)| { } }; - rsx!(cx, body { - link { - rel: "stylesheet" - href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" - integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" - crossorigin: "anonymous" - } - margin_left: "35%" - h1 {"Dioxus CRM Example"} - {scene} - }) + cx.render(rsx!( + body { + link { + rel: "stylesheet" + href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" + integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" + crossorigin: "anonymous" + } + margin_left: "35%" + h1 {"Dioxus CRM Example"} + {scene} + } + )) }; diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index becf386f5..d14e1fecb 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -102,12 +102,9 @@ pub static Example: FC<()> = |(cx, props)| { }} // Matching - // Matching will throw a Rust error about "no two closures are the same type" - // To fix this, call "render" method or use the "in" syntax to produce VNodes. - // There's nothing we can do about it, sorry :/ (unless you want *really* unhygienic macros) {match true { - true => rsx!(cx, h1 {"Top text"}), - false => cx.render(rsx!( h1 {"Bottom text"})) + true => rsx!( h1 {"Top text"}), + false => rsx!( h1 {"Bottom text"}) }} // Conditional rendering @@ -174,10 +171,17 @@ pub static Example: FC<()> = |(cx, props)| { // Can take children too! Taller { a: "asd", div {"hello world!"} } + + // helper functions + {helper(cx, "hello world!")} } }) }; +fn helper(cx: Context, text: &str) -> Element { + rsx!(cx, p { "{text}" }) +} + mod baller { use super::*; #[derive(Props, PartialEq)] diff --git a/examples/ssr.rs b/examples/ssr.rs index 2823e00fc..9c30d1a20 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -21,6 +21,6 @@ static App: FC<()> = |(cx, props)| { struct MyProps<'a> { text: &'a str, } -fn App2<'a>(cx: Context<'a>, props: &'a MyProps) -> Element<'a> { +fn App2(cx: Context, props: &MyProps) -> Element { None } diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 2682b9f9b..5d77bea10 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -24,14 +24,16 @@ 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} + } }) }; diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 0a13b1bcd..babaee825 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -210,12 +210,12 @@ impl ToTokens for ElementAttrNamed { ElementAttr::EventClosure { name, closure } => { quote! { - dioxus::events::on::#name(__cx, #closure) + dioxus_elements::on::#name(__cx, #closure) } } ElementAttr::EventTokens { name, tokens } => { quote! { - dioxus::events::on::#name(__cx, #tokens) + dioxus_elements::on::#name(__cx, #tokens) } } }; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 8088b55d4..2bffae33d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -40,8 +40,6 @@ indexmap = "1.7.0" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } -serde_repr = { version = "0.1.7", optional = true } - [dev-dependencies] anyhow = "1.0.42" dioxus-html = { path = "../html" } @@ -54,7 +52,7 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.2" } [features] default = [] -serialize = ["serde", "serde_repr"] +serialize = ["serde"] debug_vdom = [] [[bench]] diff --git a/packages/core/README.md b/packages/core/README.md index f02ec114c..45059cd89 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -40,3 +40,19 @@ We have big goals for Dioxus. The final implementation must: - Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. - Be modular. Components and hooks should be work anywhere without worrying about target platform. + + +## Safety + +Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe. + +If you don't want to use a crate that uses unsafe, then this crate is not for you. + +however, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. + +We'd also be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous variants required to execute certain features. + +There's a few invariants that are very important: + +- References to `ScopeInner` and `Props` passed into components are *always* valid for as long as the component exists. Even if the scope backing is resized to fit more scopes, the scope has to stay the same place in memory. + diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 3348f246a..a3e69b662 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -99,3 +99,105 @@ Internally, the flow of suspense works like this: 9. diff that node with the new node with a low priority on its own fiber 10. return the patches back to the event loop 11. apply the patches to the real dom + +/* +Welcome to Dioxus's cooperative, priority-based scheduler. + +I hope you enjoy your stay. + +Some essential reading: +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200 +- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440 +- https://github.com/WICG/is-input-pending +- https://web.dev/rail/ +- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react + +# What's going on? + +Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI +snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no +point in being "fast" if you can't also be "responsive." + +As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned +priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms). +The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many +different types of work simultaneously. + +# How does it work? + +Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked. +React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of +React, and the UI still stays snappy! + +While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do +this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser +has time to paint and run animations. When the browser is finished, we can step in and continue our work. + +React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not +necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially +"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause +our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields +so Rust's async machinery can take over, allowing us to customize when exactly to pause it. + +React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For +now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes +before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes +are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine +future is paused and saved (self-referentially). + +# Priority System + +So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that +doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where +priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as +a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work. + +React's priority system is quite complex. + +There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only +uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on. + +For Dioxus, we're going with a 4 tier priority system: +- Sync: Things that need to be done by the next frame, like TextInput on controlled elements +- High: for events that block all others - clicks, keyboard, and hovers +- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events) +- Low: set_state called asynchronously, and anything generated by suspense + +In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed +work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave +as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to +do anything too arduous from onInput. + +For the rest, we defer to the rIC period and work down each queue from high to low. +*/ + + + +Strategy: +- When called, check for any UI events that might've been received since the last frame. +- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue. + +- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high," + then we need to finish the high priority work first. If the current work is "low" then analyze what scopes + will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need + to bump the other work out of the way, or choose to process it so we don't have any conflicts. + 'static components have a leg up here since their work can be re-used among multiple scopes. + "High priority" is only for blocking! Should only be used on "clicks" + +- If there are no pending discrete events, then check for continuous events. These can be completely batched + +- we batch completely until we run into a discrete event +- all continuous events are batched together +- so D C C C C C would be two separate events - D and C. IE onclick and onscroll +- D C C C C C C D C C C D would be D C D C D in 5 distinct phases. + +- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future + - we need to keep track of element parents to be able to traverse properly + + +Open questions: +- what if we get two clicks from the component during the same slice? + - should we batch? + - react says no - they are continuous + - but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff? + diff --git a/packages/core/benches/jsframework.rs b/packages/core/benches/jsframework.rs index d0c8aae77..a36d4a1e5 100644 --- a/packages/core/benches/jsframework.rs +++ b/packages/core/benches/jsframework.rs @@ -24,7 +24,7 @@ criterion_group!(mbenches, create_rows); criterion_main!(mbenches); fn create_rows(c: &mut Criterion) { - static App: FC<()> = |(cx, _)| { + static App: FC<()> = |cx, _| { let mut rng = SmallRng::from_entropy(); let rows = (0..10_000_usize).map(|f| { let label = Label::new(&mut rng); @@ -58,7 +58,7 @@ struct RowProps { row_id: usize, label: Label, } -fn Row((cx, props): Scope) -> Element { +fn Row(cx: Context, props: &RowProps) -> Element { let [adj, col, noun] = props.label.0; cx.render(rsx! { tr { diff --git a/packages/core/examples/props_expand.rs b/packages/core/examples/props_expand.rs new file mode 100644 index 000000000..29d41129e --- /dev/null +++ b/packages/core/examples/props_expand.rs @@ -0,0 +1,9 @@ +use dioxus_core as dioxus; +use dioxus_core_macro::*; + +fn main() {} + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a str, +} diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs new file mode 100644 index 000000000..45104daa0 --- /dev/null +++ b/packages/core/examples/works.rs @@ -0,0 +1,44 @@ +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +fn main() { + let _ = VirtualDom::new(Parent); +} + +fn Parent(cx: Context, props: &()) -> Element { + let value = cx.use_hook(|_| String::new(), |f| &*f); + + cx.render(rsx! { + div { + Child { name: value } + Fragment { "asd" } + } + }) +} + +#[derive(Props)] +struct ChildProps<'a> { + name: &'a str, +} + +fn Child(cx: Context, props: &ChildProps) -> Element { + cx.render(rsx! { + div { + h1 { "it's nested" } + Child2 { name: props.name } + } + }) +} + +#[derive(Props)] +struct Grandchild<'a> { + name: &'a str, +} + +fn Child2(cx: Context, props: &Grandchild) -> Element { + cx.render(rsx! { + div { "Hello {props.name}!" } + }) +} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index e249bfae1..82818ab62 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -5,43 +5,7 @@ //! if the type supports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder //! that ensures compile-time required and optional fields on cx. -use crate::{ - innerlude::{Context, Element, VAnchor, VFragment, VNode}, - LazyNodes, ScopeChildren, -}; -/// A component is a wrapper around a Context and some Props that share a lifetime -/// -/// -/// # Example -/// -/// With memoized state: -/// ```rust -/// struct State {} -/// -/// fn Example((cx, props): Scope) -> DomTree { -/// // ... -/// } -/// ``` -/// -/// With borrowed state: -/// ```rust -/// struct State<'a> { -/// name: &'a str -/// } -/// -/// fn Example<'a>((cx, props): Scope<'a, State>) -> DomTree<'a> { -/// // ... -/// } -/// ``` -/// -/// With owned state as a closure: -/// ```rust -/// static Example: FC<()> = |(cx, props)| { -/// // ... -/// }; -/// ``` -/// -pub type Scope<'a, T> = (Context<'a>, &'a T); +use crate::innerlude::{Context, Element, LazyNodes, ScopeChildren}; pub struct FragmentProps<'a> { children: ScopeChildren<'a>, @@ -51,7 +15,7 @@ pub struct FragmentBuilder<'a, const BUILT: bool> { children: Option>, } impl<'a> FragmentBuilder<'a, false> { - pub fn children(mut self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> { + pub fn children(self, children: ScopeChildren<'a>) -> FragmentBuilder<'a, true> { FragmentBuilder { children: Some(children), } @@ -75,7 +39,7 @@ impl<'a> Properties for FragmentProps<'a> { FragmentBuilder { children: None } } - unsafe fn memoize(&self, other: &Self) -> bool { + unsafe fn memoize(&self, _other: &Self) -> bool { false } } @@ -102,7 +66,7 @@ impl<'a> Properties for FragmentProps<'a> { /// 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> { +pub fn Fragment<'a>(cx: Context<'a>, props: &'a FragmentProps<'a>) -> Element { cx.render(Some(LazyNodes::new(|f| { f.fragment_from_iter(&props.children) }))) @@ -173,6 +137,7 @@ impl EmptyBuilder { /// 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 { + // pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder { T::builder() } diff --git a/packages/core/src/coroutines.rs b/packages/core/src/coroutines.rs deleted file mode 100644 index 490a0acab..000000000 --- a/packages/core/src/coroutines.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Coroutines are just a "futures unordered" buffer for tasks that can be submitted through the use_coroutine hook. -//! -//! The idea here is to move *coroutine* support as a layer on top of *tasks* - -use futures_util::{stream::FuturesUnordered, Future}; - -pub struct CoroutineScheduler { - futures: FuturesUnordered>>, -} - -impl CoroutineScheduler { - pub fn new() -> Self { - CoroutineScheduler { - futures: FuturesUnordered::new(), - } - } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 654048267..4f923af24 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -90,6 +90,7 @@ use crate::innerlude::*; use fxhash::{FxHashMap, FxHashSet}; +use slab::Slab; use DomEdit::*; /// Our DiffMachine is an iterative tree differ. @@ -104,76 +105,33 @@ 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> { pub mutations: Mutations<'bump>, - pub stack: DiffStack<'bump>, + pub(crate) stack: DiffStack<'bump>, pub seen_scopes: FxHashSet, - pub cfg: DiffCfg, -} - -pub(crate) struct DiffCfg { pub force_diff: bool, } -impl Default for DiffCfg { - fn default() -> Self { - Self { - force_diff: Default::default(), - } - } -} -/// a "saved" form of a diff machine -/// in regular diff machine, the &'bump reference is a stack borrow, but the -/// bump lifetimes are heap borrows. -pub(crate) struct SavedDiffWork<'bump> { - pub mutations: Mutations<'bump>, - pub stack: DiffStack<'bump>, - pub seen_scopes: FxHashSet, -} - -impl<'a> SavedDiffWork<'a> { - pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> { - std::mem::transmute(self) - } - - pub unsafe fn promote<'b>(self, vdom: &'b ResourcePool) -> DiffMachine<'b> { - let extended: SavedDiffWork<'b> = std::mem::transmute(self); - DiffMachine { - vdom, - cfg: DiffCfg::default(), - mutations: extended.mutations, - stack: extended.stack, - seen_scopes: extended.seen_scopes, - } - } -} - -impl<'bump> DiffMachine<'bump> { - pub(crate) fn new(mutations: Mutations<'bump>, shared: &'bump ResourcePool) -> Self { +impl<'bump> DiffState<'bump> { + pub(crate) fn new(mutations: Mutations<'bump>) -> Self { Self { mutations, - cfg: DiffCfg::default(), stack: DiffStack::new(), - vdom: shared, - seen_scopes: FxHashSet::default(), + seen_scopes: Default::default(), + force_diff: false, } } +} - pub fn save(self) -> SavedDiffWork<'bump> { - SavedDiffWork { - mutations: self.mutations, - stack: self.stack, - seen_scopes: self.seen_scopes, - } - } +impl<'bump> ScopeArena { + pub fn diff_scope(&'bump self, state: &mut DiffState<'bump>, id: &ScopeId) { + // if let Some(component) = self.get_scope(id) { - 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); - } + let (old, new) = (self.wip_head(id), self.fin_head(id)); + + state.stack.push(DiffInstruction::Diff { old, new }); + self.work(state, || false); + // } } /// Progress the diffing for this "fiber" @@ -183,14 +141,21 @@ impl<'bump> DiffMachine<'bump> { /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). /// /// Returns a `bool` indicating that the work completed properly. - pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool { - while let Some(instruction) = self.stack.pop() { + pub fn work( + &'bump self, + state: &mut DiffState<'bump>, + mut deadline_expired: impl FnMut() -> bool, + ) -> bool { + while let Some(instruction) = state.stack.pop() { match instruction { - DiffInstruction::Diff { old, new } => self.diff_node(old, new), - DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => self.prepare_move_node(node), - DiffInstruction::PopScope => self.stack.pop_off_scope(), + DiffInstruction::Diff { old, new } => self.diff_node(state, old, new), + DiffInstruction::Create { node } => self.create_node(state, node), + DiffInstruction::Mount { and } => self.mount(state, and), + DiffInstruction::PrepareMove { node } => { + let num_on_stack = self.push_all_nodes(state, node); + state.stack.add_child_count(num_on_stack); + } + DiffInstruction::PopScope => state.stack.pop_off_scope(), }; if deadline_expired() { @@ -202,48 +167,84 @@ 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( + &'bump self, + state: &mut DiffState<'bump>, + node: &'bump VNode<'bump>, + ) -> usize { + match node { + VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => { + state.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Linked(linked) => { + todo!("load linked"); + 0 + // let num_on_stack = linked.children.iter().map(|child| { + // self.push_all_nodes(state, child) + // }).sum(); + // state.mutations.push_root(node.mounted_id()); + // num_on_stack + 1 + } + + VNode::Fragment(_) | VNode::Component(_) => { + // + let mut added = 0; + for child in node.children() { + added += self.push_all_nodes(state, child); + } + added + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(state, child); + } + state.mutations.push_root(el.dom_id.get().unwrap()); + + num_on_stack + 1 + } } } - fn mount(&mut self, and: MountType<'bump>) { - let nodes_created = self.stack.pop_nodes_created(); + fn mount(&'bump self, state: &mut DiffState<'bump>, and: MountType<'bump>) { + let nodes_created = state.stack.pop_nodes_created(); match and { // add the nodes from this virtual list to the parent // used by fragments and components MountType::Absorb => { - self.stack.add_child_count(nodes_created); + state.stack.add_child_count(nodes_created); } MountType::Replace { old } => { if let Some(old_id) = old.try_mounted_id() { - self.mutations.replace_with(old_id, nodes_created as u32); - self.remove_nodes(Some(old), true); + state.mutations.replace_with(old_id, nodes_created as u32); + self.remove_nodes(state, Some(old), true); } else { if let Some(id) = self.find_first_element_id(old) { - self.mutations.replace_with(id, nodes_created as u32); + state.mutations.replace_with(id, nodes_created as u32); } - self.remove_nodes(Some(old), true); + self.remove_nodes(state, Some(old), true); } } MountType::Append => { - self.mutations.edits.push(AppendChildren { + state.mutations.edits.push(AppendChildren { many: nodes_created as u32, }); } MountType::InsertAfter { other_node } => { let root = self.find_last_element(other_node).unwrap(); - self.mutations.insert_after(root, nodes_created as u32); + state.mutations.insert_after(root, nodes_created as u32); } MountType::InsertBefore { other_node } => { let root = self.find_first_element_id(other_node).unwrap(); - self.mutations.insert_before(root, nodes_created as u32); + state.mutations.insert_before(root, nodes_created as u32); } } } @@ -252,42 +253,63 @@ impl<'bump> DiffMachine<'bump> { // Tools for creating new nodes // ================================= - fn create_node(&mut self, node: &'bump VNode<'bump>) { + fn create_node(&'bump self, state: &mut DiffState<'bump>, node: &'bump VNode<'bump>) { match node { - VNode::Text(vtext) => self.create_text_node(vtext, node), - VNode::Suspended(suspended) => self.create_suspended_node(suspended, node), - VNode::Anchor(anchor) => self.create_anchor_node(anchor, node), - VNode::Element(element) => self.create_element_node(element, node), - VNode::Fragment(frag) => self.create_fragment_node(frag), - VNode::Component(component) => self.create_component_node(component), + VNode::Text(vtext) => self.create_text_node(state, vtext, node), + VNode::Suspended(suspended) => self.create_suspended_node(state, suspended, node), + VNode::Anchor(anchor) => self.create_anchor_node(state, anchor, node), + VNode::Element(element) => self.create_element_node(state, element, node), + VNode::Fragment(frag) => self.create_fragment_node(state, frag), + VNode::Component(component) => self.create_component_node(state, component), + VNode::Linked(linked) => self.create_linked_node(state, linked), } } - fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { - let real_id = self.vdom.reserve_node(node); - self.mutations.create_text_node(vtext.text, real_id); + fn create_text_node( + &'bump self, + state: &mut DiffState<'bump>, + vtext: &'bump VText<'bump>, + node: &'bump VNode<'bump>, + ) { + let real_id = self.reserve_node(node); + state.mutations.create_text_node(vtext.text, real_id); vtext.dom_id.set(Some(real_id)); - self.stack.add_child_count(1); + state.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); - self.mutations.create_placeholder(real_id); + fn create_suspended_node( + &'bump self, + state: &mut DiffState<'bump>, + suspended: &'bump VSuspended, + node: &'bump VNode<'bump>, + ) { + let real_id = self.reserve_node(node); + state.mutations.create_placeholder(real_id); suspended.dom_id.set(Some(real_id)); - self.stack.add_child_count(1); + state.stack.add_child_count(1); - self.attach_suspended_node_to_scope(suspended); + self.attach_suspended_node_to_scope(state, suspended); } - fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) { - let real_id = self.vdom.reserve_node(node); - self.mutations.create_placeholder(real_id); + fn create_anchor_node( + &'bump self, + state: &mut DiffState<'bump>, + anchor: &'bump VAnchor, + node: &'bump VNode<'bump>, + ) { + let real_id = self.reserve_node(node); + state.mutations.create_placeholder(real_id); anchor.dom_id.set(Some(real_id)); - self.stack.add_child_count(1); + state.stack.add_child_count(1); } - fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) { + fn create_element_node( + &'bump self, + state: &mut DiffState<'bump>, + element: &'bump VElement<'bump>, + node: &'bump VNode<'bump>, + ) { let VElement { tag_name, listeners, @@ -298,75 +320,90 @@ impl<'bump> DiffMachine<'bump> { .. } = element; - let real_id = self.vdom.reserve_node(node); + let real_id = self.reserve_node(node); dom_id.set(Some(real_id)); - self.mutations.create_element(tag_name, *namespace, real_id); + state + .mutations + .create_element(tag_name, *namespace, real_id); - self.stack.add_child_count(1); + state.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(); + if let Some(cur_scope_id) = state.stack.current_scope() { + let scope = self.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); - }); + state.mutations.new_event_listener(listener, cur_scope_id); + } } else { log::warn!("create element called with no scope on the stack - this is an error for a live dom"); } for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); + state.mutations.set_attribute(attr, real_id.as_u64()); } if !children.is_empty() { - self.stack.create_children(children, MountType::Append); + state.stack.create_children(children, MountType::Append); } } - fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - self.stack.create_children(frag.children, MountType::Absorb); + fn create_fragment_node( + &'bump self, + state: &mut DiffState<'bump>, + frag: &'bump VFragment<'bump>, + ) { + state + .stack + .create_children(frag.children, MountType::Absorb); } - fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let caller = vcomponent.caller; + fn create_component_node( + &'bump self, + state: &mut DiffState<'bump>, + vcomponent: &'bump VComponent<'bump>, + ) { + // let caller = vcomponent.caller; - let parent_idx = self.stack.current_scope().unwrap(); + let parent_idx = state.stack.current_scope().unwrap(); - let shared = self.vdom.channel.clone(); + let shared = self.sender.clone(); // Insert a new scope into our component list - let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); + let parent_scope = self.get_scope(&parent_idx).unwrap(); - 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 new_idx: ScopeId = todo!(); + // self + // .new_with_key(fc_ptr, vcomp, parent_scope, height, subtree, sender); + + // .(|new_idx| { + // // ScopeInner::new( + // vcomponent, + // new_idx, + // Some(parent_idx), + // parent_scope.height + 1, + // parent_scope.subtree(), + // shared, + // // ) + // }); // 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.get_scope(&parent_idx).unwrap(); + let extended = unsafe { std::mem::transmute(vcomponent) }; + cur_scope.items.get_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.get_scope(&new_idx).unwrap(); log::debug!( "initializing component {:?} with height {:?}", @@ -375,58 +412,79 @@ impl<'bump> DiffMachine<'bump> { ); // Run the scope for one iteration to initialize it - if new_component.run_scope(self.vdom) { - // Take the node that was just generated from running the component - let nextnode = new_component.frames.fin_head(); - self.stack.create_component(new_idx, nextnode); + // + todo!("run scope"); + // if new_component.run_scope(self) { + // // Take the node that was just generated from running the component + // let nextnode = new_component.frames.fin_head(); + // state.stack.create_component(new_idx, nextnode); - // - /* - tree_item { + // // + // /* + // tree_item { - } + // } - */ - if new_component.is_subtree_root.get() { - self.stack.push_subtree(); - } - } + // */ + // if new_component.is_subtree_root.get() { + // state.stack.push_subtree(); + // } + // } // Finally, insert this scope as a seen node. - self.seen_scopes.insert(new_idx); + state.seen_scopes.insert(new_idx); + } + + fn create_linked_node(&'bump self, state: &mut DiffState<'bump>, link: &'bump NodeLink) { + todo!() } // ================================= // Tools for diffing nodes // ================================= - pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { + pub fn diff_node( + &'bump self, + state: &mut DiffState<'bump>, + old_node: &'bump VNode<'bump>, + new_node: &'bump VNode<'bump>, + ) { use VNode::*; match (old_node, new_node) { // Check the most common cases first - (Text(old), Text(new)) => self.diff_text_nodes(old, new), + (Text(old), Text(new)) => self.diff_text_nodes(state, old, new), (Component(old), Component(new)) => { - self.diff_component_nodes(old_node, new_node, old, new) + self.diff_component_nodes(state, old_node, new_node, old, new) } - (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), + (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(state, old, new), (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), + (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(state, old, new), + (Element(old), Element(new)) => { + self.diff_element_nodes(state, old, new, old_node, new_node) + } + (Linked(old), Linked(new)) => self.diff_linked_nodes(state, old, new), // Anything else is just a basic replace and create ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) | Suspended(_), - ) => self + Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) + | Suspended(_), + Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_) + | Suspended(_), + ) => state .stack .create_node(new_node, MountType::Replace { old: old_node }), } } - fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { + fn diff_text_nodes( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump VText<'bump>, + new: &'bump VText<'bump>, + ) { if let Some(root) = old.dom_id.get() { if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); + state.mutations.set_text(new.text, root.as_u64()); } new.dom_id.set(Some(root)); @@ -434,7 +492,8 @@ impl<'bump> DiffMachine<'bump> { } fn diff_element_nodes( - &mut self, + &'bump self, + state: &mut DiffState<'bump>, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>, old_node: &'bump VNode<'bump>, @@ -449,11 +508,11 @@ impl<'bump> DiffMachine<'bump> { if new.tag_name != old.tag_name || new.namespace != old.namespace { // maybe make this an instruction? // issue is that we need the "vnode" but this method only has the velement - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { + state.stack.push_nodes_created(0); + state.stack.push(DiffInstruction::Mount { and: MountType::Replace { old: old_node }, }); - self.create_element_node(new, new_node); + self.create_element_node(state, new, new_node); return; } @@ -473,15 +532,15 @@ impl<'bump> DiffMachine<'bump> { if old.attributes.len() == new.attributes.len() { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value || new_attr.is_volatile { - self.mutations.set_attribute(new_attr, root.as_u64()); + state.mutations.set_attribute(new_attr, root.as_u64()); } } } else { for attribute in old.attributes { - self.mutations.remove_attribute(attribute, root.as_u64()); + state.mutations.remove_attribute(attribute, root.as_u64()); } for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()) + state.mutations.set_attribute(attribute, root.as_u64()) } } @@ -493,44 +552,47 @@ impl<'bump> DiffMachine<'bump> { // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) // // TODO: take a more efficient path than this - if let Some(cur_scope_id) = self.stack.current_scope() { - let scope = self.vdom.get_scope(cur_scope_id).unwrap(); + if let Some(cur_scope_id) = state.stack.current_scope() { + let scope = self.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()) { if old_l.event != new_l.event { - self.mutations + state + .mutations .remove_event_listener(old_l.event, root.as_u64()); - self.mutations.new_event_listener(new_l, cur_scope_id); + state.mutations.new_event_listener(new_l, cur_scope_id); } new_l.mounted_node.set(old_l.mounted_node.get()); self.attach_listener_to_scope(new_l, scope); } } else { for listener in old.listeners { - self.mutations + state + .mutations .remove_event_listener(listener.event, root.as_u64()); } for listener in new.listeners { listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); + state.mutations.new_event_listener(listener, cur_scope_id); self.attach_listener_to_scope(listener, scope); } } } if old.children.is_empty() && !new.children.is_empty() { - self.mutations.edits.push(PushRoot { + state.mutations.edits.push(PushRoot { root: root.as_u64(), }); - self.stack.create_children(new.children, MountType::Append); + state.stack.create_children(new.children, MountType::Append); } else { - self.diff_children(old.children, new.children); + self.diff_children(state, old.children, new.children); } } fn diff_component_nodes( - &mut self, + &'bump self, + state: &mut DiffState<'bump>, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>, @@ -543,50 +605,74 @@ impl<'bump> DiffMachine<'bump> { if old.user_fc == new.user_fc { log::debug!("Diffing component {:?} - {:?}", new.user_fc, scope_addr); // - self.stack.scope_stack.push(scope_addr); + state.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 = self.get_scope(&scope_addr).unwrap(); + let mut items = scope.items.borrow_mut(); // React doesn't automatically memoize, but we do. - let props_are_the_same = old.comparator.unwrap(); + let props_are_the_same = todo!("reworking component memoization"); + // let props_are_the_same = todo!("reworking component memoization"); + // 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 self.cfg.force_diff || !props_are_the_same(new) { + // let succeeded = scope.run_scope(self); - if succeeded { - self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); - } - } + // if succeeded { + // self.diff_node(scope.frames.wip_head(), scope.frames.fin_head()); + // } + // } - self.stack.scope_stack.pop(); + state.stack.scope_stack.pop(); } else { - self.stack + state + .stack .create_node(new_node, MountType::Replace { old: old_node }); } } - fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'bump>) { + fn diff_fragment_nodes( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump VFragment<'bump>, + new: &'bump VFragment<'bump>, + ) { // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff if old.children.len() == 1 && new.children.len() == 1 { - self.diff_node(&old.children[0], &new.children[0]); + self.diff_node(state, &old.children[0], &new.children[0]); return; } debug_assert!(!old.children.is_empty()); debug_assert!(!new.children.is_empty()); - self.diff_children(old.children, new.children); + self.diff_children(state, old.children, new.children); } - fn diff_suspended_nodes(&mut self, old: &'bump VSuspended, new: &'bump VSuspended) { + fn diff_suspended_nodes( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump VSuspended, + new: &'bump VSuspended, + ) { new.dom_id.set(old.dom_id.get()); - self.attach_suspended_node_to_scope(new); + self.attach_suspended_node_to_scope(state, new); + } + + fn diff_linked_nodes( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump NodeLink, + new: &'bump NodeLink, + ) { + todo!(); + // new.dom_id.set(old.dom_id.get()); + // self.attach_linked_node_to_scope(state, new); } // ============================================= @@ -608,26 +694,32 @@ impl<'bump> DiffMachine<'bump> { // // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only // to an element, and appending makes sense. - fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_children( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) { // Remember, fragments can never be empty (they always have a single child) match (old, new) { ([], []) => {} ([], _) => { // we need to push the - self.stack.create_children(new, MountType::Append); + state.stack.create_children(new, MountType::Append); } (_, []) => { - self.remove_nodes(old, true); + self.remove_nodes(state, old, true); } ([VNode::Anchor(old_anchor)], [VNode::Anchor(new_anchor)]) => { old_anchor.dom_id.set(new_anchor.dom_id.get()); } ([VNode::Anchor(_)], _) => { - self.stack + state + .stack .create_children(new, MountType::Replace { old: &old[0] }); } (_, [VNode::Anchor(_)]) => { - self.replace_and_create_many_with_one(old, &new[0]); + self.replace_and_create_many_with_one(state, old, &new[0]); } _ => { let new_is_keyed = new[0].key().is_some(); @@ -643,9 +735,9 @@ impl<'bump> DiffMachine<'bump> { ); if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); + self.diff_keyed_children(state, old, new); } else { - self.diff_non_keyed_children(old, new); + self.diff_non_keyed_children(state, old, new); } } } @@ -659,20 +751,25 @@ impl<'bump> DiffMachine<'bump> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_non_keyed_children( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); for (new, old) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::Diff { new, old }); + state.stack.push(DiffInstruction::Diff { new, old }); } use std::cmp::Ordering; match old.len().cmp(&new.len()) { - Ordering::Greater => self.remove_nodes(&old[new.len()..], true), + Ordering::Greater => self.remove_nodes(state, &old[new.len()..], true), Ordering::Less => { - self.stack.create_children( + state.stack.create_children( &new[old.len()..], MountType::InsertAfter { other_node: old.last().unwrap(), @@ -701,7 +798,12 @@ impl<'bump> DiffMachine<'bump> { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_children( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) { if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { @@ -729,7 +831,7 @@ impl<'bump> DiffMachine<'bump> { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + let (left_offset, right_offset) = match self.diff_keyed_ends(state, old, new) { Some(count) => count, None => return, }; @@ -739,7 +841,7 @@ impl<'bump> DiffMachine<'bump> { // right_offset, // ); - // log::debug!("stack before lo is {:#?}", self.stack.instructions); + // log::debug!("stack before lo is {:#?}", state.stack.instructions); // Ok, we now hopefully have a smaller range of children in the middle // within which to re-order nodes with the same keys, remove old nodes with // now-unused keys, and create new nodes with fresh keys. @@ -753,14 +855,14 @@ impl<'bump> DiffMachine<'bump> { ); if new_middle.is_empty() { // remove the old elements - self.remove_nodes(old_middle, true); + self.remove_nodes(state, old_middle, true); } else if old_middle.is_empty() { // there were no old elements, so just create the new elements // we need to find the right "foothold" though - we shouldn't use the "append" at all if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.stack.create_children( + state.stack.create_children( new_middle, MountType::InsertBefore { other_node: foothold, @@ -769,7 +871,7 @@ impl<'bump> DiffMachine<'bump> { } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.stack.create_children( + state.stack.create_children( new_middle, MountType::InsertAfter { other_node: foothold, @@ -778,7 +880,7 @@ impl<'bump> DiffMachine<'bump> { } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.stack.create_children( + state.stack.create_children( new_middle, MountType::InsertAfter { other_node: foothold, @@ -786,10 +888,10 @@ impl<'bump> DiffMachine<'bump> { ); } } else { - self.diff_keyed_middle(old_middle, new_middle); + self.diff_keyed_middle(state, old_middle, new_middle); } - log::debug!("stack after km is {:#?}", self.stack.instructions); + log::debug!("stack after km is {:#?}", state.stack.instructions); } /// Diff both ends of the children that share keys. @@ -798,7 +900,8 @@ 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, + &'bump self, + state: &mut DiffState<'bump>, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], ) -> Option<(usize, usize)> { @@ -809,14 +912,14 @@ impl<'bump> DiffMachine<'bump> { if old.key() != new.key() { break; } - self.stack.push(DiffInstruction::Diff { old, new }); + state.stack.push(DiffInstruction::Diff { old, new }); left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.stack.create_children( + state.stack.create_children( &new[left_offset..], MountType::InsertAfter { other_node: old.last().unwrap(), @@ -828,7 +931,7 @@ impl<'bump> DiffMachine<'bump> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if left_offset == new.len() { - self.remove_nodes(&old[left_offset..], true); + self.remove_nodes(state, &old[left_offset..], true); return None; } @@ -839,7 +942,7 @@ impl<'bump> DiffMachine<'bump> { if old.key() != new.key() { break; } - self.diff_node(old, new); + self.diff_node(state, old, new); right_offset += 1; } @@ -859,7 +962,12 @@ 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. - fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_middle( + &'bump self, + state: &mut DiffState<'bump>, + old: &'bump [VNode<'bump>], + new: &'bump [VNode<'bump>], + ) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -923,7 +1031,7 @@ 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); + self.replace_and_create_many_with_many(state, old, new); return; } @@ -967,15 +1075,15 @@ impl<'bump> DiffMachine<'bump> { // add mount instruction for the last items not covered by the lis let first_lis = *lis_sequence.first().unwrap(); if first_lis > 0 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { + state.stack.push_nodes_created(0); + state.stack.push(DiffInstruction::Mount { and: MountType::InsertBefore { other_node: &new[first_lis], }, }); for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { - apply(idx, new_node, &mut self.stack); + apply(idx, new_node, &mut state.stack); } } @@ -984,14 +1092,14 @@ impl<'bump> DiffMachine<'bump> { let mut last = *lis_iter.next().unwrap(); for next in lis_iter { if last - next > 1 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { + state.stack.push_nodes_created(0); + state.stack.push(DiffInstruction::Mount { and: MountType::InsertBefore { other_node: &new[last], }, }); for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { - apply(idx + next + 1, new_node, &mut self.stack); + apply(idx + next + 1, new_node, &mut state.stack); } } last = *next; @@ -1000,19 +1108,19 @@ impl<'bump> DiffMachine<'bump> { // add mount instruction for the first items not covered by the lis let last = *lis_sequence.last().unwrap(); if last < (new.len() - 1) { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { + state.stack.push_nodes_created(0); + state.stack.push(DiffInstruction::Mount { and: MountType::InsertAfter { other_node: &new[last], }, }); for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { - apply(idx + last + 1, new_node, &mut self.stack); + apply(idx + last + 1, new_node, &mut state.stack); } } for idx in lis_sequence.iter().rev() { - self.stack.push(DiffInstruction::Diff { + state.stack.push(DiffInstruction::Diff { new: &new[*idx], old: &old[new_index_to_old_index[*idx]], }); @@ -1023,7 +1131,7 @@ impl<'bump> DiffMachine<'bump> { // Utilities // ===================== - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> Option { + fn find_last_element(&'bump self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { @@ -1032,20 +1140,22 @@ 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(_) => { + todo!() + } 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()); + // let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(self.root_node(&scope_id)); } } } } - fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { + fn find_first_element_id(&'bump self, vnode: &'bump VNode<'bump>) -> Option { let mut search_node = Some(vnode); loop { @@ -1056,8 +1166,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()); + // let scope = self.get_scope(&scope_id).unwrap(); + search_node = Some(self.root_node(&scope_id)); + } + VNode::Linked(link) => { + todo!("linked") } VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), @@ -1068,23 +1181,26 @@ impl<'bump> DiffMachine<'bump> { } fn replace_and_create_many_with_one( - &mut self, + &'bump self, + state: &mut DiffState<'bump>, old: &'bump [VNode<'bump>], new: &'bump VNode<'bump>, ) { if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - self.stack + self.remove_nodes(state, &old[1..], true); + state + .stack .create_node(new, MountType::Replace { old: first_old }); } else { - self.stack.create_node(new, MountType::Append {}); + state.stack.create_node(new, MountType::Append {}); } } /// schedules nodes for garbage collection and pushes "remove" to the mutation stack /// remove can happen whenever fn remove_nodes( - &mut self, + &'bump self, + state: &mut DiffState<'bump>, nodes: impl IntoIterator>, gen_muts: bool, ) { @@ -1093,50 +1209,54 @@ impl<'bump> DiffMachine<'bump> { match node { VNode::Text(t) => { let id = t.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + self.collect_garbage(id); if gen_muts { - self.mutations.remove(id.as_u64()); + state.mutations.remove(id.as_u64()); } } VNode::Suspended(s) => { let id = s.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + self.collect_garbage(id); if gen_muts { - self.mutations.remove(id.as_u64()); + state.mutations.remove(id.as_u64()); } } VNode::Anchor(a) => { let id = a.dom_id.get().unwrap(); - self.vdom.collect_garbage(id); + self.collect_garbage(id); if gen_muts { - self.mutations.remove(id.as_u64()); + state.mutations.remove(id.as_u64()); } } VNode::Element(e) => { let id = e.dom_id.get().unwrap(); if gen_muts { - self.mutations.remove(id.as_u64()); + state.mutations.remove(id.as_u64()); } - self.remove_nodes(e.children, false); + self.remove_nodes(state, e.children, false); } VNode::Fragment(f) => { - self.remove_nodes(f.children, gen_muts); + self.remove_nodes(state, f.children, gen_muts); + } + + VNode::Linked(l) => { + todo!() } 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); + // let scope = self.get_scope(&scope_id).unwrap(); + let root = self.root_node(&scope_id); + self.remove_nodes(state, Some(root), gen_muts); log::debug!("Destroying scope {:?}", scope_id); - let mut s = self.vdom.try_remove(scope_id).unwrap(); + let mut s = self.try_remove(&scope_id).unwrap(); s.hooks.clear_hooks(); } } @@ -1147,38 +1267,44 @@ impl<'bump> DiffMachine<'bump> { /// /// The new nodes *will* be created - don't create them yourself! fn replace_and_create_many_with_many( - &mut self, + &'bump self, + state: &mut DiffState<'bump>, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>], ) { if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - self.stack + self.remove_nodes(state, &old[1..], true); + state + .stack .create_children(new, MountType::Replace { old: first_old }) } else { - self.stack.create_children(new, MountType::Append {}); + state.stack.create_children(new, MountType::Append {}); } } /// 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(&'bump 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 + fn attach_suspended_node_to_scope( + &'bump self, + state: &mut DiffState<'bump>, + suspended: &'bump VSuspended, + ) { + if let Some(scope) = state .stack .current_scope() - .and_then(|id| self.vdom.get_scope_mut(id)) + .and_then(|id| self.get_scope(&id)) { // safety: this lifetime is managed by the logic on scope - let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) }; + let extended = unsafe { std::mem::transmute(suspended) }; scope - .suspended_nodes + .items .borrow_mut() - .insert(suspended.task_id, extended as *const _); + .suspended_nodes + .insert(suspended.task_id, extended); } } } diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs index 8685824a1..8eaa42a5c 100644 --- a/packages/core/src/diff_stack.rs +++ b/packages/core/src/diff_stack.rs @@ -49,10 +49,6 @@ impl<'bump> DiffStack<'bump> { } } - pub fn is_empty(&self) -> bool { - self.instructions.is_empty() - } - pub fn pop(&mut self) -> Option> { self.instructions.pop() } diff --git a/packages/core/src/heuristics.rs b/packages/core/src/heuristics.rs deleted file mode 100644 index a8069c95e..000000000 --- a/packages/core/src/heuristics.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::collections::HashMap; - -use fxhash::FxHashMap; - -use crate::FC; - -/// Provides heuristics to the "SharedResources" object for improving allocation performance. -/// -/// This heuristics engine records the memory footprint of bump arenas and hook lists for each component. These records are -/// then used later on to optimize the initial allocation for future components. This helps save large allocations later on -/// that would slow down the diffing and initialization process. -/// -/// -pub struct HeuristicsEngine { - heuristics: FxHashMap, -} - -pub type FcSlot = *const (); - -pub struct Heuristic { - hooks: u32, - bump_size: u64, -} - -impl HeuristicsEngine { - pub(crate) fn new() -> Self { - Self { - heuristics: FxHashMap::default(), - } - } - - fn recommend(&mut self, fc: FC, heuristic: Heuristic) { - let g = fc as FcSlot; - let e = self.heuristics.entry(g); - } - - fn get_recommendation(&mut self, fc: FC) -> &Heuristic { - let id = fc as FcSlot; - - self.heuristics.entry(id).or_insert(Heuristic { - bump_size: 100, - hooks: 10, - }) - } -} - -#[test] -fn types_work() { - let engine = HeuristicsEngine::new(); -} diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs index ed4eb49dd..d3b79ca48 100644 --- a/packages/core/src/hooklist.rs +++ b/packages/core/src/hooklist.rs @@ -1,10 +1,9 @@ use std::{ any::Any, - cell::{Cell, RefCell, UnsafeCell}, + cell::{Cell, RefCell}, }; -type UnsafeInnerHookState = UnsafeCell>; -type HookCleanup = Box)>; +use bumpalo::Bump; /// An abstraction over internally stored data using a hook-based memory layout. /// @@ -15,7 +14,8 @@ type HookCleanup = Box)>; /// Todo: this could use its very own bump arena, but that might be a tad overkill #[derive(Default)] pub(crate) struct HookList { - vals: RefCell>, + arena: Bump, + vals: RefCell>, idx: Cell, } @@ -23,7 +23,7 @@ impl HookList { pub(crate) fn next(&self) -> Option<&mut T> { self.vals.borrow().get(self.idx.get()).and_then(|inn| { self.idx.set(self.idx.get() + 1); - let raw_box = unsafe { &mut *inn.0.get() }; + let raw_box = unsafe { &mut **inn }; raw_box.downcast_mut::() }) } @@ -34,14 +34,13 @@ impl HookList { /// /// 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) { + pub(crate) unsafe fn reset(&self) { self.idx.set(0); } - pub(crate) fn push_hook(&self, new: T, cleanup: HookCleanup) { - self.vals - .borrow_mut() - .push((UnsafeCell::new(Box::new(new)), cleanup)) + pub(crate) fn push_hook(&self, new: T) { + let val = self.arena.alloc(new); + self.vals.borrow_mut().push(val) } pub(crate) fn len(&self) -> usize { @@ -57,10 +56,16 @@ impl HookList { } pub fn clear_hooks(&mut self) { - log::debug!("clearing hooks..."); - self.vals - .borrow_mut() - .drain(..) - .for_each(|(state, cleanup)| cleanup(state.into_inner())); + 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() } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 19f56cf58..72bb8dfb7 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -27,89 +27,118 @@ pub struct LazyNodes<'a, 'b> { inner: StackNodeStorage<'a, 'b>, } -type StackHeapSize = [usize; 0]; +type StackHeapSize = [usize; 8]; enum StackNodeStorage<'a, 'b> { Stack(LazyStack), - Heap(Box) -> VNode<'a> + 'b>), + Heap(Box>) -> Option> + 'b>), } impl<'a, 'b> LazyNodes<'a, 'b> { - pub fn new(val: F) -> Self + pub fn new(_val: F) -> Self where F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, { - unsafe { - let mut ptr: *const _ = &val as &dyn FnOnce(NodeFactory<'a>) -> VNode<'a>; + // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch + let mut slot = Some(_val); - assert_eq!( - ptr as *const u8, &val as *const _ as *const u8, - "MISUSE: Closure returned different pointer" - ); - assert_eq!( - std::mem::size_of_val(&*ptr), - std::mem::size_of::(), - "MISUSE: Closure returned a subset pointer" - ); - let words = ptr_as_slice(&mut ptr); - assert!( - words[0] == &val as *const _ as usize, - "BUG: Pointer layout is not (data_ptr, info...)" + let val = move |fac: Option>| { + let inner = slot.take().unwrap(); + fac.map(inner) + }; + + unsafe { LazyNodes::new_inner(val) } + } + + unsafe fn new_inner(val: F) -> Self + where + F: FnMut(Option>) -> Option> + 'b, + { + let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; + + assert_eq!( + ptr as *const u8, &val as *const _ as *const u8, + "MISUSE: Closure returned different pointer" + ); + assert_eq!( + std::mem::size_of_val(&*ptr), + std::mem::size_of::(), + "MISUSE: Closure returned a subset pointer" + ); + + let words = ptr_as_slice(&mut ptr); + assert!( + words[0] == &val as *const _ as usize, + "BUG: Pointer layout is not (data_ptr, info...)" + ); + + // - Ensure that Self is aligned same as data requires + assert!( + std::mem::align_of::() <= std::mem::align_of::(), + "TODO: Enforce alignment >{} (requires {})", + std::mem::align_of::(), + std::mem::align_of::() + ); + + let info = &words[1..]; + let data = words[0] as *mut (); + let size = mem::size_of::(); + + let stored_size = info.len() * mem::size_of::() + size; + let max_size = mem::size_of::(); + + if stored_size > max_size { + log::debug!( + "lazy nodes was too large to fit into stack. falling back to heap. max: {}, actual {:?}", + max_size, + stored_size + ); + + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } + } else { + log::debug!( + "lazy nodes fits on stack! max: {}, actual: {:?}", + max_size, + stored_size ); + let mut buf: StackHeapSize = StackHeapSize::default(); - // - Ensure that Self is aligned same as data requires - assert!( - std::mem::align_of::() <= std::mem::align_of::(), - "TODO: Enforce alignment >{} (requires {})", - std::mem::align_of::(), - std::mem::align_of::() - ); + assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); - let info = &words[1..]; - let data = words[0] as *mut (); - let size = mem::size_of::(); - - if info.len() * mem::size_of::() + size > mem::size_of::() { - log::debug!("lazy nodes was too large to fit into stack. falling back to heap"); - - Self { - inner: StackNodeStorage::Heap(Box::new(val)), + // 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 +146,51 @@ impl<'a, 'b> LazyNodes<'a, 'b> { struct LazyStack { _align: [u64; 0], buf: StackHeapSize, + dropped: bool, } impl LazyStack { - unsafe fn create_boxed<'a>(&mut self) -> Box) -> VNode<'a>> { + fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { let LazyStack { buf, .. } = self; let data = buf.as_ref(); - let info_size = mem::size_of::<*mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a>>() - / mem::size_of::() - - 1; + let info_size = + mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() + / mem::size_of::() + - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnOnce(NodeFactory<'a>) -> VNode<'a> = - make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]); + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - Box::from_raw(g) - } + self.dropped = true; - fn call(mut self, f: NodeFactory) -> VNode { - let boxed = unsafe { self.create_boxed() }; - boxed(f) + let clos = unsafe { &mut *g }; + clos(Some(f)).unwrap() } } impl Drop for LazyStack { fn drop(&mut self) { - let boxed = unsafe { self.create_boxed() }; - mem::drop(boxed); + if !self.dropped { + let LazyStack { buf, .. } = self; + let data = buf.as_ref(); + + let info_size = mem::size_of::< + *mut dyn FnMut(Option>) -> Option>, + >() / mem::size_of::() + - 1; + + let info_ofs = data.len() - info_size; + + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; + + self.dropped = true; + + let clos = unsafe { &mut *g }; + clos(None); + } } } @@ -177,7 +223,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 +237,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 { bump: &bump }; + + struct DropInner { + id: i32, + } + impl Drop for DropInner { + fn drop(&mut self) { + log::debug!("dropping inner"); + } + } + let val = Rc::new(10); + + { + let it = (0..10) + .map(|i| { + let val = val.clone(); + + NodeFactory::annotate_lazy(move |f| { + log::debug!("hell closure"); + let inner = DropInner { id: i }; + f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) + }) + }) + .collect::>(); + + let caller = NodeFactory::annotate_lazy(|f| { + log::debug!("main closure"); + f.fragment_from_iter(it) + }) + .unwrap(); + } + + // let nodes = caller.call(factory); + + // dbg!(nodes); + + dbg!(Rc::strong_count(&val)); +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d023b2c37..6c9ef181e 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,68 +12,49 @@ Navigating this crate: Some utilities */ -pub(crate) mod bumpframe; -pub(crate) mod childiter; pub(crate) mod component; -pub(crate) mod coroutines; pub(crate) mod diff; pub(crate) mod diff_stack; -pub(crate) mod events; -pub(crate) mod heuristics; pub(crate) mod hooklist; -pub(crate) mod hooks; pub(crate) mod lazynodes; pub(crate) mod mutations; pub(crate) mod nodes; -pub(crate) mod resources; -pub(crate) mod scheduler; pub(crate) mod scope; -pub(crate) mod tasks; +pub(crate) mod scopearena; pub(crate) mod test_dom; -pub(crate) mod threadsafe; pub(crate) mod util; pub(crate) mod virtual_dom; -#[cfg(feature = "debug_vdom")] -pub mod debug_dom; - pub(crate) mod innerlude { - pub(crate) use crate::bumpframe::*; - pub(crate) use crate::childiter::*; pub use crate::component::*; pub(crate) use crate::diff::*; pub use crate::diff_stack::*; - pub use crate::events::*; - pub use crate::heuristics::*; pub(crate) use crate::hooklist::*; - pub use crate::hooks::*; pub use crate::lazynodes::*; pub use crate::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::scopearena::*; pub use crate::test_dom::*; - pub use crate::threadsafe::*; pub use crate::util::*; pub use crate::virtual_dom::*; - pub type Element<'a> = Option>; - pub type FC

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

= for<'a> fn(Context<'a>, &'a P) -> Element; } pub use crate::innerlude::{ - Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType, - Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, TaskHandle, TestDom, - ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, + Attribute, Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, + Listener, MountType, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, + TestDom, UserEvent, VAnchor, VElement, VFragment, VNode, VSuspended, VirtualDom, FC, }; pub mod prelude { - pub use crate::component::{fc_to_builder, Fragment, Properties, Scope}; - pub use crate::hooks::*; + pub use crate::component::{fc_to_builder, Fragment, Properties}; pub use crate::innerlude::Context; - pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, ScopeChildren, FC}; + pub use crate::innerlude::{ + DioxusElement, Element, LazyNodes, NodeFactory, Scope, ScopeChildren, FC, + }; pub use crate::nodes::VNode; pub use crate::VirtualDom; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1cb06a4d6..a81aac3b6 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner}, + innerlude::{empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId}, lazynodes::LazyNodes, }; use bumpalo::{boxed::Box as BumpBox, Bump}; @@ -14,6 +14,19 @@ use std::{ fmt::{Arguments, Debug, Formatter}, }; +/// 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. +#[derive(Clone, Debug)] +pub struct NodeLink { + pub(crate) link_idx: usize, + pub(crate) gen_id: u32, + pub(crate) scope_id: ScopeId, +} + /// 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: @@ -118,6 +131,13 @@ pub enum VNode<'src> { /// } /// ``` Anchor(&'src VAnchor), + + /// A type of node that links this node to another scope or render cycle + /// + /// Is essentially a "pointer" to a "rendered" node in a particular scope + /// + /// Used in portals + Linked(NodeLink), } impl<'src> VNode<'src> { @@ -127,9 +147,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, } } @@ -149,11 +171,20 @@ 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 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 { @@ -165,7 +196,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 { + gen_id: c.gen_id, + scope_id: c.scope_id, + link_idx: c.link_idx, }), } } @@ -175,17 +210,24 @@ 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 {{ gen_id: {}, scope_id: {:?} }}", + c.gen_id, c.scope_id + ), } } } @@ -209,8 +251,6 @@ 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. @@ -309,6 +349,7 @@ pub struct Listener<'bump> { pub(crate) callback: RefCell) + 'bump>>>, } +pub type VCompCaller<'src> = BumpBox<'src, dyn Fn(Context) -> Element + 'src>; /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { @@ -316,21 +357,19 @@ pub struct VComponent<'src> { pub associated_scope: Cell>, - pub is_static: bool, - // Function pointer to the FC that was used to generate this component pub user_fc: *const (), - pub(crate) caller: BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>, - - pub(crate) comparator: Option bool + 'src>>, - - pub(crate) drop_props: RefCell>>, - pub(crate) can_memoize: bool, + pub(crate) hard_allocation: Cell>, + // Raw pointer into the bump arena for the props of the component - pub(crate) raw_props: *const (), + pub(crate) bump_props: *const (), + + // during the "teardown" process we'll take the caller out so it can be dropped properly + pub(crate) caller: Option>, + pub(crate) comparator: Option bool + 'src>>, } pub struct VSuspended<'a> { @@ -338,7 +377,7 @@ pub struct VSuspended<'a> { pub dom_id: Cell>, #[allow(clippy::type_complexity)] - pub callback: RefCell Element<'a> + 'a>>>, + pub callback: RefCell Element + 'a>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -475,16 +514,30 @@ impl<'a> NodeFactory<'a> { pub fn component

( &self, - component: fn(Scope<'a, P>) -> Element<'a>, + component: fn(Context<'a>, &'a P) -> Element, props: P, key: Option, ) -> VNode<'a> where P: Properties + 'a, { + /* + our strategy: + - unsafe hackery + - lol + + - we don't want to hit the global allocator + - allocate into our bump arena + - if the props aren't static, then we convert them into a box which we pass off between renders + */ + + // let bump = self.bump(); + + // // later, we'll do a hard allocation + // let raw_ptr = bump.alloc(props); 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: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| { @@ -496,7 +549,7 @@ 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) }; @@ -508,7 +561,6 @@ impl<'a> NodeFactory<'a> { } } }); - let comparator = Some(unsafe { BumpBox::from_raw(comparator) }); let drop_props = { // create a closure to drop the props @@ -518,7 +570,7 @@ impl<'a> NodeFactory<'a> { 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); @@ -536,33 +588,41 @@ 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 { + let caller: &'a mut dyn Fn(&Scope) -> Element = + bump.alloc(move |scope: &Scope| -> 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> = (scp, props); - - let res: Element = component(s); + let props: &'_ P = unsafe { &*(bump_props as *const P) }; + let res = component(scope, props); + // let res = component((Context { scope }, props)); unsafe { std::mem::transmute(res) } }); - let caller = unsafe { BumpBox::from_raw(caller) }; + let can_memoize = P::IS_STATIC; VNode::Component(bump.alloc(VComponent { user_fc, comparator, - raw_props, + bump_props, caller, - is_static: P::IS_STATIC, key, - can_memoize: P::IS_STATIC, + can_memoize, drop_props, associated_scope: Cell::new(None), })) } + pub fn listener( + self, + event: &'static str, + callback: BumpBox<'a, dyn FnMut(Box) + 'a>, + ) -> Listener<'a> { + Listener { + mounted_node: Cell::new(None), + event, + callback: RefCell::new(Some(callback)), + } + } + pub fn fragment_from_iter( self, node_iter: impl IntoIterator>, @@ -604,14 +664,12 @@ impl<'a> NodeFactory<'a> { VNode::Fragment(VFragment { children, key: None, - is_static: false, }) } - pub fn annotate_lazy<'z, 'b, F>(f: F) -> Option> - where - F: FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b, - { + pub fn annotate_lazy<'z, 'b>( + f: impl FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b, + ) -> Option> { Some(LazyNodes::new(f)) } } @@ -672,6 +730,21 @@ impl IntoVNode<'_> for Option<()> { } } +// Conveniently, we also support "None" +impl IntoVNode<'_> for Option { + fn into_vnode(self, cx: NodeFactory) -> VNode { + todo!() + // cx.fragment_from_iter(None as Option) + } +} +// Conveniently, we also support "None" +impl IntoVNode<'_> for NodeLink { + fn into_vnode(self, cx: NodeFactory) -> VNode { + todo!() + // cx.fragment_from_iter(None as Option) + } +} + impl<'a> IntoVNode<'a> for Option> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { self.unwrap_or_else(|| cx.fragment_from_iter(None as Option)) @@ -684,7 +757,6 @@ impl<'a> IntoVNode<'a> for Option> { Some(lazy) => lazy.call(cx), None => VNode::Fragment(VFragment { children: &[], - is_static: false, key: None, }), } diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/old/bumpframe.rs similarity index 55% rename from packages/core/src/bumpframe.rs rename to packages/core/src/old/bumpframe.rs index ac1c66189..1c1994e8d 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/old/bumpframe.rs @@ -3,69 +3,51 @@ 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, + pub cur_generation: Cell, // The double-buffering situation that we will use pub frames: [BumpFrame; 2], } -pub(crate) struct BumpFrame { - pub bump: Bump, - pub(crate) head_node: VNode<'static>, - - #[cfg(test)] - // used internally for debugging - _name: &'static str, -} - impl ActiveFrame { pub fn new() -> Self { let b1 = Bump::new(); let b2 = Bump::new(); let frame_a = BumpFrame { - head_node: VNode::Fragment(VFragment { - key: None, - children: &[], - is_static: false, - }), bump: b1, - - #[cfg(test)] - _name: "wip", + generation: 0.into(), }; 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(), + }; + + Self { frames: [frame_a, frame_b], + cur_generation: 0.into(), } } - pub unsafe fn reset_wip_frame(&mut self) { - self.wip_frame_mut().bump.reset() + pub unsafe fn reset_wip_frame(&self) { + // todo: unsafecell or something + let bump = self.wip_frame() as *const _ as *mut BumpFrame; + let g = &mut *bump; + g.bump.reset(); + + // 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 { + match self.cur_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 { + match self.cur_generation.get() & 1 == 0 { true => &mut self.frames[0], false => &mut self.frames[1], } @@ -73,26 +55,26 @@ impl ActiveFrame { /// 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 { + match self.cur_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 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) } - } + // /// 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); + self.cur_generation.set(self.cur_generation.get() + 1); } } @@ -119,6 +101,6 @@ mod tests { assert_eq!(fin._name, "wip"); frames.cycle_frame(); } - assert_eq!(frames.generation.get(), 10); + assert_eq!(frames.cur_generation.get(), 10); } } diff --git a/packages/core/src/childiter.rs b/packages/core/src/old/childiter.rs similarity index 97% rename from packages/core/src/childiter.rs rename to packages/core/src/old/childiter.rs index 0dc047caf..b7b6a70ea 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/old/childiter.rs @@ -62,7 +62,7 @@ impl<'a> Iterator for RealChildIterator<'a> { VNode::Component(sc) => { let scope = self .scopes - .get_scope(sc.associated_scope.get().unwrap()) + .get_scope(&sc.associated_scope.get().unwrap()) .unwrap(); // Simply swap the current node on the stack with the root of the component diff --git a/packages/core/src/debug_dom.rs b/packages/core/src/old/debug_dom.rs similarity index 94% rename from packages/core/src/debug_dom.rs rename to packages/core/src/old/debug_dom.rs index 13d874be3..0ffa4214a 100644 --- a/packages/core/src/debug_dom.rs +++ b/packages/core/src/old/debug_dom.rs @@ -1,4 +1,4 @@ -use crate::{innerlude::ScopeInner, virtual_dom::VirtualDom, VNode}; +use crate::{innerlude::ScopeState, virtual_dom::VirtualDom, VNode}; impl std::fmt::Display for VirtualDom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -24,7 +24,7 @@ impl std::fmt::Display for VirtualDom { pub(crate) struct ScopeRenderer<'a> { pub skip_components: bool, pub show_fragments: bool, - pub _scope: &'a ScopeInner, + pub _scope: &'a ScopeState, pub _pre_render: bool, pub _newline: bool, pub _indent: bool, @@ -48,6 +48,9 @@ impl<'a> ScopeRenderer<'a> { }; match &node { + VNode::Linked(_) => { + write!(f, "Linked").unwrap(); + } VNode::Text(text) => { write_indent(f, il); writeln!(f, "\"{}\"", text.text)? @@ -115,7 +118,7 @@ impl<'a> ScopeRenderer<'a> { VNode::Component(vcomp) => { let idx = vcomp.associated_scope.get().unwrap(); if !self.skip_components { - let new_node = vdom.get_scope(idx).unwrap().root_node(); + let new_node = vdom.get_scope(&idx).unwrap().root_node(); self.render(vdom, new_node, f, il)?; } } diff --git a/packages/core/src/old/events.rs b/packages/core/src/old/events.rs new file mode 100644 index 000000000..6038cbcb6 --- /dev/null +++ b/packages/core/src/old/events.rs @@ -0,0 +1,15 @@ +//! An event system that's less confusing than Traits + RC; +//! This should hopefully make it easier to port to other platforms. +//! +//! Unfortunately, it is less efficient than the original, but hopefully it's negligible. + +use crate::{ + innerlude::Listener, + innerlude::{ElementId, NodeFactory, ScopeId}, +}; +use bumpalo::boxed::Box as BumpBox; +use std::{ + any::Any, + cell::{Cell, RefCell}, + fmt::Debug, +}; diff --git a/packages/core/src/hooks.rs b/packages/core/src/old/hooks.rs similarity index 58% rename from packages/core/src/hooks.rs rename to packages/core/src/old/hooks.rs index b18a445c1..349011dae 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/old/hooks.rs @@ -13,73 +13,73 @@ 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_coroutine<'src, Out, Fut, Init>( - cx: Context<'src>, - task_initializer: Init, -) -> (&'src TaskHandle, &'src Option) -where - Out: 'static, - Fut: Future + 'static, - Init: FnOnce() -> Fut + 'src, -{ - struct TaskHook { - handle: TaskHandle, - task_dump: Rc>>, - value: Option, - } +// /// 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_coroutine<'src, Out, Fut, Init>( +// cx: Context<'src>, +// task_initializer: Init, +// ) -> (&'src TaskHandle, &'src Option) +// where +// Out: 'static, +// Fut: Future + 'static, +// Init: FnOnce() -> Fut + 'src, +// { +// struct TaskHook { +// handle: TaskHandle, +// task_dump: Rc>>, +// value: Option, +// } - todo!() +// todo!() - // // whenever the task is complete, save it into th - // cx.use_hook( - // move |_| { - // let task_fut = task_initializer(); +// // // 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 task_dump = Rc::new(RefCell::new(None)); - // let slot = task_dump.clone(); +// // let slot = task_dump.clone(); - // let updater = cx.schedule_update_any(); - // let originator = cx.scope.our_arena_idx; +// // 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 - // }))); +// // 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) - // }, - // |_| {}, - // ) -} +// // 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. /// @@ -95,11 +95,11 @@ pub fn use_suspense<'src, Out, Fut, Cb>( cx: Context<'src>, task_initializer: impl FnOnce() -> Fut, user_callback: Cb, -) -> Element<'src> +) -> Element where Fut: Future, Out: 'static, - Cb: FnMut(&Out) -> Element<'src> + 'src, + Cb: FnMut(&Out) -> Element + 'src, { /* General strategy: @@ -167,10 +167,10 @@ where // ) } -pub(crate) struct SuspenseHook { - pub handle: TaskHandle, - pub value: Rc>>>, -} +// pub(crate) struct SuspenseHook { +// pub handle: TaskHandle, +// pub value: Rc>>>, +// } #[derive(Clone, Copy)] pub struct NodeRef<'src, T: 'static>(&'src RefCell>); @@ -183,5 +183,5 @@ impl<'a, T> Deref for NodeRef<'a, T> { } pub fn use_node_ref(cx: Context) -> NodeRef { - cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {}) + cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }) } diff --git a/packages/core/src/noderef.rs b/packages/core/src/old/noderef.rs similarity index 100% rename from packages/core/src/noderef.rs rename to packages/core/src/old/noderef.rs diff --git a/packages/core/src/resources.rs b/packages/core/src/old/resources.rs similarity index 92% rename from packages/core/src/resources.rs rename to packages/core/src/old/resources.rs index 4163e30d3..df931dcda 100644 --- a/packages/core/src/resources.rs +++ b/packages/core/src/old/resources.rs @@ -32,18 +32,18 @@ pub(crate) struct ResourcePool { 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> { + 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> { + 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 { + pub fn try_remove(&self, id: &ScopeId) -> Option { let inner = unsafe { &mut *self.components.get() }; Some(inner.remove(id.0)) // .try_remove(id.0) diff --git a/packages/core/src/scheduler.rs b/packages/core/src/old/scheduler.rs similarity index 80% rename from packages/core/src/scheduler.rs rename to packages/core/src/old/scheduler.rs index f6c5ee018..21503fffd 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/old/scheduler.rs @@ -68,14 +68,17 @@ 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 bumpalo::Bump; 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::pin::Pin; +use std::task::Poll; use std::{ any::{Any, TypeId}, cell::{Cell, UnsafeCell}, @@ -85,11 +88,7 @@ use std::{ #[derive(Clone)] pub(crate) struct EventChannel { - pub task_counter: Rc>, - pub cur_subtree: Rc>, pub sender: UnboundedSender, - pub schedule_any_immediate: Rc, - pub submit_task: Rc TaskHandle>, pub get_shared_context: GetSharedContext, } @@ -101,16 +100,6 @@ pub enum SchedulerMsg { // setstate Immediate(ScopeId), - - // tasks - Task(TaskMsg), -} - -pub enum TaskMsg { - ToggleTask(u64), - PauseTask(u64), - ResumeTask(u64), - DropTask(u64), } /// The scheduler holds basically everything around "working" @@ -126,16 +115,26 @@ pub enum TaskMsg { /// /// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. /// +/// There's a lot of raw pointers here... +/// +/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable +/// The best way to do that is a bump allocator. +/// +/// /// 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, + // /// 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 component_arena: Bump, - pub heuristics: HeuristicsEngine, + pub free_components: VecDeque<*mut ScopeInner>, + + pub heuristics: FxHashMap, pub receiver: UnboundedReceiver, @@ -145,104 +144,70 @@ pub(crate) struct Scheduler { // Every component that has futures that need to be polled pub pending_futures: FxHashSet, - // In-flight futures - pub async_tasks: FuturesUnordered, - // // scheduler stuff // pub current_priority: EventPriority, pub ui_events: VecDeque, pub pending_immediates: VecDeque, - pub pending_tasks: VecDeque, - pub batched_events: VecDeque, pub garbage_scopes: HashSet, pub dirty_scopes: IndexSet, + pub saved_state: Option>, + pub in_progress: bool, } +pub type FcSlot = *const (); + +pub struct Heuristic { + hook_arena_size: usize, + node_arena_size: usize, +} + impl Scheduler { pub(crate) fn new( sender: UnboundedSender, receiver: UnboundedReceiver, + component_capacity: usize, + element_capacity: usize, ) -> 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 components = Rc::new(UnsafeCell::new(Slab::with_capacity(component_capacity))); + let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(element_capacity))); 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 - }) + todo!() + // 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 pool = ResourcePool { + // components, + // raw_elements, + // channel, + // }; let saved_state = SavedDiffWork { mutations: Mutations::new(), @@ -251,22 +216,15 @@ impl Scheduler { }; Self { - pool, - + // 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(), @@ -275,6 +233,8 @@ impl Scheduler { dirty_scopes: Default::default(), saved_state: Some(saved_state), in_progress: false, + + heuristics: todo!(), } } @@ -282,7 +242,7 @@ impl Scheduler { 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(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { // TODO: bubble properly here scope.call_listener(event, element); @@ -308,7 +268,7 @@ impl Scheduler { 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) {} + // if let Some(scope) = self.get_scope_mut(&trigger.scope) {} // } } @@ -328,15 +288,9 @@ impl Scheduler { 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) => { @@ -344,7 +298,7 @@ impl Scheduler { let (discrete, priority) = event_meta(&event); - if let Some(scope) = self.pool.get_scope_mut(event.scope) { + if let Some(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { // TODO: bubble properly here scope.call_listener(event, element); @@ -374,19 +328,19 @@ impl Scheduler { 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 shared = self.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(); + let shared = self.clone(); self.dirty_scopes - .retain(|id| shared.get_scope(*id).is_some()); + .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; + let h1 = shared.get_scope(a).unwrap().height; + let h2 = shared.get_scope(b).unwrap().height; h1.cmp(&h2).reverse() }); @@ -396,8 +350,8 @@ impl Scheduler { 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) { + if let Some(component) = self.get_scope_mut(&scopeid) { + if component.run_scope(&self) { let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); // let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); @@ -494,7 +448,6 @@ impl Scheduler { 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); } @@ -506,7 +459,7 @@ impl Scheduler { // 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(scope) = self.get_scope_mut(&event.scope) { if let Some(element) = event.mounted_dom_id { log::info!("Calling listener {:?}, {:?}", event.scope, element); @@ -519,7 +472,6 @@ impl Scheduler { self.dirty_scopes.insert(im); } SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e), - SchedulerMsg::Task(_) => todo!(), } } } @@ -562,19 +514,19 @@ impl Scheduler { /// /// 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 shared = self.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) + .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) { + if cur_component.run_scope(&self) { diff_machine .stack .create_node(cur_component.frames.fin_head(), MountType::Append); @@ -596,13 +548,13 @@ impl Scheduler { pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations { let cur_component = self .pool - .get_scope_mut(base_scope) + .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); + if cur_component.run_scope(&self) { + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self); diff_machine.cfg.force_diff = true; diff_machine.diff_scope(base_scope); diff_machine.mutations @@ -611,3 +563,35 @@ impl Scheduler { } } } + +impl Future for Scheduler { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let mut all_pending = true; + + for fut in self.pending_futures.iter() { + let scope = self + .pool + .get_scope_mut(&fut) + .expect("Scope should never be moved"); + + let items = scope.items.get_mut(); + for task in items.tasks.iter_mut() { + let t = task.as_mut(); + let g = unsafe { Pin::new_unchecked(t) }; + match g.poll(cx) { + Poll::Ready(r) => { + all_pending = false; + } + Poll::Pending => {} + } + } + } + + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + } +} diff --git a/packages/core/src/threadsafe.rs b/packages/core/src/old/threadsafe.rs similarity index 100% rename from packages/core/src/threadsafe.rs rename to packages/core/src/old/threadsafe.rs diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index f48163710..789946016 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,44 +1,36 @@ use crate::innerlude::*; +use futures_channel::mpsc::UnboundedSender; use fxhash::FxHashMap; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, collections::HashMap, future::Future, - pin::Pin, rc::Rc, }; -use crate::{innerlude::*, lazynodes::LazyNodes}; use bumpalo::{boxed::Box as BumpBox, Bump}; -use std::ops::Deref; /// 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 { +/// #[derive(Props)] +/// struct ExampleProps { /// name: String /// } /// -/// fn example(cx: Context) -> VNode { -/// html! { -///

"Hello, {cx.name}"
-/// } +/// fn Example((cx, props): Scope) -> Element { +/// cx.render(rsx!{ div {"Hello, {props.name}"} }) /// } /// ``` -pub type Context<'a> = &'a ScopeInner; +pub type Context<'a> = &'a Scope; /// Every component in Dioxus is represented by a `Scope`. /// @@ -49,17 +41,33 @@ pub type Context<'a> = &'a ScopeInner; /// /// We expose the `Scope` type so downstream users can traverse the Dioxus VirtualDOM for whatever /// use case they might have. -pub struct ScopeInner { +pub struct Scope { // Book-keeping about our spot in the arena - pub(crate) parent_idx: Option, + + // safety: + // + // pointers to scopes are *always* valid since they are bump allocated and never freed until this scope is also freed + // this is just a bit of a hack to not need an Rc to the ScopeArena. + // todo: replace this will ScopeId and provide a connection to scope arena directly + pub(crate) parent_scope: Option<*mut Scope>, + pub(crate) our_arena_idx: ScopeId, + pub(crate) height: u32, + pub(crate) subtree: Cell, + pub(crate) is_subtree_root: Cell, - // Nodes - pub(crate) frames: ActiveFrame, - pub(crate) caller: BumpBox<'static, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>, + pub(crate) generation: Cell, + + // The double-buffering situation that we will use + pub(crate) frames: [BumpFrame; 2], + + pub(crate) old_root: RefCell>, + pub(crate) new_root: RefCell>, + + pub(crate) caller: *mut dyn Fn(&Scope) -> Element, /* we care about: @@ -67,12 +75,7 @@ pub struct ScopeInner { - borrowed props (and how to drop them when the parent is dropped) - suspended nodes (and how to call their callback when their associated tasks are complete) */ - pub(crate) listeners: RefCell>>, - pub(crate) borrowed_props: RefCell>>, - pub(crate) suspended_nodes: RefCell>>, - - pub(crate) tasks: RefCell>>>, - pub(crate) pending_effects: RefCell>>, + pub(crate) items: RefCell>, // State pub(crate) hooks: HookList, @@ -80,34 +83,19 @@ pub struct ScopeInner { // todo: move this into a centralized place - is more memory efficient pub(crate) shared_contexts: RefCell>>, - // whenever set_state is called, we fire off a message to the scheduler - // this closure _is_ the method called by schedule_update that marks this component as dirty - pub(crate) memoized_updater: Rc, - - pub(crate) shared: EventChannel, + pub(crate) sender: UnboundedSender, } -/// Public interface for Scopes. -impl ScopeInner { - /// Get the root VNode for this Scope. - /// - /// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment. - /// - /// # Example - /// ```rust - /// let mut dom = VirtualDom::new(|(cx, props)|cx.render(rsx!{ div {} })); - /// dom.rebuild(); - /// - /// let base = dom.base_scope(); - /// - /// if let VNode::VElement(node) = base.root_node() { - /// assert_eq!(node.tag_name, "div"); - /// } - /// ``` - pub fn root_node(&self) -> &VNode { - self.frames.fin_head() - } +pub struct SelfReferentialItems<'a> { + pub(crate) listeners: Vec<&'a Listener<'a>>, + pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, + pub(crate) suspended_nodes: FxHashMap>, + pub(crate) tasks: Vec>>, + pub(crate) pending_effects: Vec>, +} +// 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 @@ -128,16 +116,6 @@ impl ScopeInner { self.subtree.get() } - pub(crate) fn new_subtree(&self) -> Option { - if self.is_subtree_root.get() { - None - } else { - let cur = self.shared.cur_subtree.get(); - self.shared.cur_subtree.set(cur + 1); - Some(cur) - } - } - /// Get the height of this Scope - IE the number of scopes above it. /// /// A Scope with a height of `0` is the root scope - there are no other scopes above it. @@ -173,7 +151,10 @@ impl ScopeInner { /// assert_eq!(base.parent(), None); /// ``` pub fn parent(&self) -> Option { - self.parent_idx + match self.parent_scope { + Some(p) => Some(unsafe { &*p }.our_arena_idx), + None => None, + } } /// Get the ID of this Scope within this Dioxus VirtualDOM. @@ -192,233 +173,17 @@ 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>>; - -/// 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: BumpBox Fn(&'b ScopeInner) -> Element<'b>>, - our_arena_idx: ScopeId, - parent_idx: Option, - height: u32, - subtree: u32, - shared: EventChannel, - ) -> Self { - let schedule_any_update = shared.schedule_any_immediate.clone(); - - let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); - - // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph - let caller = unsafe { std::mem::transmute(caller) }; - - Self { - memoized_updater, - shared, - caller, - parent_idx, - our_arena_idx, - height, - subtree: Cell::new(subtree), - is_subtree_root: Cell::new(false), - tasks: Default::default(), - frames: ActiveFrame::new(), - hooks: Default::default(), - - pending_effects: Default::default(), - suspended_nodes: Default::default(), - shared_contexts: Default::default(), - listeners: Default::default(), - borrowed_props: Default::default(), - } - } - - 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) }; - } - - /// 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(&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"); - - if let Some(scope) = pool.get_scope_mut(scope_id) { - scope.ensure_drop_safety(pool); - - let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); - drop_props(); - } - }); - - // Now that all the references are gone, we can safely drop our own references in our listeners. - self.listeners - .get_mut() - .drain(..) - .map(|li| unsafe { &*li }) - .for_each(|listener| drop(listener.callback.borrow_mut().take())); - } - - /// A safe wrapper around calling listeners - pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) { - let listners = self.listeners.borrow_mut(); - - let raw_listener = listners.iter().find(|lis| { - let search = unsafe { &***lis }; - if search.event == event.name { - let search_id = search.mounted_node.get(); - search_id.map(|f| f == element).unwrap_or(false) - } else { - false - } - }); - - if let Some(raw_listener) = raw_listener { - let listener = unsafe { &**raw_listener }; - let mut cb = listener.callback.borrow_mut(); - if let Some(cb) = cb.as_mut() { - (cb)(event.event); - } - } else { - log::warn!("An event was triggered but there was no listener to handle it"); - } - } - - /* - 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(); - - 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 mut boxed = sus.callback.borrow_mut().take().unwrap(); - let new_node: Element<'a> = boxed(); - } - } - - // run the list of effects - pub(crate) fn run_effects(&mut self, pool: &ResourcePool) { - todo!() - // let mut effects = self.frames.effects.borrow_mut(); - // let mut effects = effects.drain(..).collect::>(); - - // for effect in effects { - // let effect = unsafe { &*effect }; - // let effect = effect.as_ref(); - - // let mut effect = effect.borrow_mut(); - // let mut effect = effect.as_mut(); - - // effect.run(pool); - // } - } - - /// Render this component. - /// - /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated). - pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { - // Cycle to the next frame and then reset it - // This breaks any latent references, invalidating every pointer referencing into it. - // Remove all the outdated listeners - self.ensure_drop_safety(pool); - - // Safety: - // - We dropped the listeners, so no more &mut T can be used while these are held - // - All children nodes that rely on &mut T are replaced with a new reference - unsafe { self.hooks.reset() }; - - // Safety: - // - We've dropped all references to the wip bump frame - unsafe { self.frames.reset_wip_frame() }; - - // just forget about our suspended nodes while we're at it - self.suspended_nodes.get_mut().clear(); - - // guarantee that we haven't screwed up - there should be no latent references anywhere - debug_assert!(self.listeners.borrow().is_empty()); - debug_assert!(self.suspended_nodes.borrow().is_empty()); - debug_assert!(self.borrowed_props.borrow().is_empty()); - - log::debug!("Borrowed stuff is successfully cleared"); - - // Cast the caller ptr from static to one with our own reference - let render: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> = unsafe { &*self.caller }; - - // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. - if let Some(builder) = render(self) { - let new_head = builder.into_vnode(NodeFactory { - bump: &self.frames.wip_frame().bump, - }); - log::debug!("Render is successful"); - - // the user's component succeeded. We can safely cycle to the next frame - self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; - self.frames.cycle_frame(); - - true - } else { - false - } - } /// Create a subscription that schedules a future render for the reference component /// /// ## Notice: you should prefer using prepare_update and get_scope_id pub fn schedule_update(&self) -> Rc { - self.memoized_updater.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 needs_update(&self) { - (self.memoized_updater)() - } - - /// 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) { - (self.shared.schedule_any_immediate)(id) + // pub fn schedule_update(&self) -> Rc { + let chan = self.sender.clone(); + let id = self.scope_id(); + Rc::new(move || { + let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) } /// Schedule an update for any component given its ScopeId. @@ -427,15 +192,31 @@ impl ScopeInner { /// /// This method should be used when you want to schedule an update for a component pub fn schedule_update_any(&self) -> Rc { - self.shared.schedule_any_immediate.clone() + let chan = self.sender.clone(); + Rc::new(move |id| { + let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); + }) + } + + /// 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()) + } + + /// 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)); } /// 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 { - let bump = &self.frames.wip_frame().bump; - bump + &self.wip_frame().bump } /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. @@ -445,21 +226,28 @@ impl ScopeInner { /// ## Example /// /// ```ignore - /// fn Component(cx: Context<()>) -> VNode { + /// fn Component(cx: Scope, props: &Props) -> Element { /// // Lazy assemble the VNode tree - /// let lazy_tree = html! {
"Hello World"
}; + /// let lazy_nodes = rsx!("hello world"); /// /// // Actually build the tree and allocate it /// cx.render(lazy_tree) /// } ///``` - pub fn render<'src>( - &'src self, - lazy_nodes: Option>, - ) -> Option> { - let bump = &self.frames.wip_frame().bump; + pub fn render<'src>(&'src self, lazy_nodes: Option>) -> Option { + let bump = &self.wip_frame().bump; let factory = NodeFactory { bump }; - lazy_nodes.map(|f| f.call(factory)) + let node = lazy_nodes.map(|f| f.call(factory))?; + + let idx = self + .wip_frame() + .add_node(unsafe { std::mem::transmute(node) }); + + Some(NodeLink { + gen_id: self.generation.get(), + scope_id: self.our_arena_idx, + link_idx: idx, + }) } /// Push an effect to be ran after the component has been successfully mounted to the dom @@ -475,8 +263,9 @@ impl ScopeInner { // erase the 'src lifetime for self-referential storage let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; - self.pending_effects.borrow_mut().push(self_ref_fut); - self.pending_effects.borrow().len() - 1 + 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 @@ -491,8 +280,9 @@ impl ScopeInner { // erase the 'src lifetime for self-referential storage let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; - self.tasks.borrow_mut().push(self_ref_fut); - self.tasks.borrow().len() - 1 + 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. @@ -519,7 +309,7 @@ impl ScopeInner { /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn provide_state(self, value: T) + pub fn provide_state(&self, value: T) where T: 'static, { @@ -531,11 +321,21 @@ impl ScopeInner { } /// Try to retrieve a SharedState with type T from the any parent Scope. - pub fn consume_state(self) -> Option> { - let getter = &self.shared.get_shared_context; - let ty = TypeId::of::(); - let idx = self.our_arena_idx; - getter(idx, ty).map(|f| f.downcast().unwrap()) + pub fn consume_state(&self) -> Option> { + if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { + Some(shared.clone().downcast::().unwrap()) + } else { + let mut search_parent = self.parent_scope; + + while let Some(parent_ptr) = search_parent { + let parent = unsafe { &*parent_ptr }; + if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { + return Some(shared.clone().downcast::().unwrap()); + } + search_parent = parent.parent_scope; + } + None + } } /// Create a new subtree with this scope as the root of the subtree. @@ -553,7 +353,7 @@ impl ScopeInner { /// rsx!(cx, div { "Subtree {id}"}) /// }; /// ``` - pub fn create_subtree(self) -> Option { + pub fn create_subtree(&self) -> Option { self.new_subtree() } @@ -570,7 +370,7 @@ impl ScopeInner { /// rsx!(cx, div { "Subtree {id}"}) /// }; /// ``` - pub fn get_current_subtree(self) -> u32 { + pub fn get_current_subtree(&self) -> u32 { self.subtree() } @@ -580,8 +380,9 @@ impl ScopeInner { /// /// - 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 /// + /// To "cleanup" the hook, implement `Drop` on the stored hook value. Whenever the component is dropped, the hook + /// will be dropped as well. /// /// # Example /// @@ -591,42 +392,132 @@ impl ScopeInner { /// use_hook( /// || Rc::new(RefCell::new(initial_value())), /// |state| state, - /// |_| {}, /// ) /// } /// ``` - pub fn use_hook<'src, State, Output, Init, Run, Cleanup>( + pub fn use_hook<'src, State: 'static, Output: 'src>( &'src self, - initializer: Init, - runner: Run, - cleanup: Cleanup, - ) -> Output - where - State: 'static, - Output: 'src, - Init: FnOnce(usize) -> State, - Run: FnOnce(&'src mut State) -> Output, - Cleanup: FnOnce(Box) + 'static, - { - // If the idx is the same as the hook length, then we need to add the current hook + 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()), - Box::new(|raw| { - let s = raw.downcast::().unwrap(); - cleanup(s); - }), - ); + self.hooks.push_hook(initializer(self.hooks.len())); } - runner(self.hooks.next::().expect(HOOK_ERR_MSG)) - } -} - -const HOOK_ERR_MSG: &str = r###" + 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. "###; + + runner(self.hooks.next::().expect(HOOK_ERR_MSG)) + } +} + +// Important internal methods +impl Scope { + /// The "work in progress frame" represents the frame that is currently being worked on. + pub(crate) fn wip_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + } + } + pub(crate) fn fin_frame(&self) -> &BumpFrame { + match self.generation.get() & 1 == 1 { + true => &self.frames[0], + false => &self.frames[1], + } + } + + pub unsafe fn reset_wip_frame(&self) { + // todo: unsafecell or something + let bump = self.wip_frame() as *const _ as *mut Bump; + let g = &mut *bump; + g.reset(); + } + + pub fn cycle_frame(&self) { + self.generation.set(self.generation.get() + 1); + } + + /// A safe wrapper around calling listeners + pub(crate) fn call_listener(&self, event: UserEvent, element: ElementId) { + let listners = &mut self.items.borrow_mut().listeners; + + 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) + } else { + false + } + }); + + if let Some(listener) = listener { + let mut cb = listener.callback.borrow_mut(); + if let Some(cb) = cb.as_mut() { + (cb)(event.event); + } + } else { + log::warn!("An event was triggered but there was no listener to handle it"); + } + } + + // 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 = &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 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) { + for mut effect in self.items.get_mut().pending_effects.drain(..) { + effect(); + } + } + + pub(crate) fn new_subtree(&self) -> Option { + todo!() + // if self.is_subtree_root.get() { + // None + // } else { + // let cur = self.shared.cur_subtree.get(); + // self.shared.cur_subtree.set(cur + 1); + // Some(cur) + // } + } +} + +pub struct BumpFrame { + pub bump: Bump, + pub nodes: RefCell>>, +} +impl BumpFrame { + pub fn new() -> Self { + let bump = Bump::new(); + + let node = &*bump.alloc(VText { + text: "asd", + dom_id: Default::default(), + is_static: false, + }); + let nodes = RefCell::new(vec![VNode::Text(unsafe { std::mem::transmute(node) })]); + Self { bump, nodes } + } + fn add_node(&self, node: VNode<'static>) -> usize { + let mut nodes = self.nodes.borrow_mut(); + nodes.push(node); + nodes.len() - 1 + } +} diff --git a/packages/core/src/scopearena.rs b/packages/core/src/scopearena.rs new file mode 100644 index 000000000..8f9050159 --- /dev/null +++ b/packages/core/src/scopearena.rs @@ -0,0 +1,290 @@ +use slab::Slab; +use std::{ + borrow::BorrowMut, + cell::{Cell, RefCell}, +}; + +use bumpalo::{boxed::Box as BumpBox, Bump}; +use futures_channel::mpsc::UnboundedSender; + +use crate::innerlude::*; + +pub type FcSlot = *const (); +// pub heuristics: FxHashMap, + +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, + scopes: Vec<*mut Scope>, + free_scopes: Vec, + nodes: RefCell>>, + pub(crate) sender: UnboundedSender, +} + +impl ScopeArena { + pub fn new(sender: UnboundedSender) -> Self { + Self { + bump: Bump::new(), + scopes: Vec::new(), + free_scopes: Vec::new(), + nodes: RefCell::new(Slab::new()), + sender, + } + } + + pub fn get_scope(&self, id: &ScopeId) -> Option<&Scope> { + unsafe { Some(&*self.scopes[id.0]) } + } + + pub fn new_with_key( + &mut self, + fc_ptr: *const (), + caller: *mut dyn Fn(&Scope) -> Element, + parent_scope: Option<*mut Scope>, + height: u32, + subtree: u32, + ) -> ScopeId { + if let Some(id) = self.free_scopes.pop() { + // have already called drop on it - the slot is still chillin tho + let scope = unsafe { &mut *self.scopes[id.0 as usize] }; + + todo!("override the scope contents"); + id + } else { + let scope_id = ScopeId(self.scopes.len()); + + let old_root = NodeLink { + link_idx: 0, + gen_id: 0, + scope_id, + }; + let new_root = NodeLink { + link_idx: 0, + gen_id: 0, + scope_id, + }; + + let new_scope = Scope { + sender: self.sender.clone(), + parent_scope, + our_arena_idx: scope_id, + height, + subtree: Cell::new(subtree), + is_subtree_root: Cell::new(false), + frames: [BumpFrame::new(), BumpFrame::new()], + + hooks: Default::default(), + shared_contexts: Default::default(), + caller, + generation: 0.into(), + + old_root: RefCell::new(Some(old_root)), + new_root: RefCell::new(Some(new_root)), + // old_root: RefCell::new(Some(old_root)), + // new_root: RefCell::new(None), + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + suspended_nodes: Default::default(), + tasks: Default::default(), + pending_effects: Default::default(), + }), + }; + + let stable = self.bump.alloc(new_scope); + self.scopes.push(stable); + scope_id + } + } + + pub fn try_remove(&self, id: &ScopeId) -> Option { + todo!() + } + + 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 = node as *const _; + let node = unsafe { std::mem::transmute(node) }; + entry.insert(node); + id + + // let nodes = self.nodes.borrow_mut(); + // let id = nodes.insert(()); + // let node_id = ElementId(id); + // node = Some(node_id); + // node_id + } + + pub fn collect_garbage(&self, id: ElementId) { + todo!() + } + + // 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(); + + // 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 + scope + .items + .borrow_mut() + .borrowed_props + .drain(..) + .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"); + + todo!("move this onto virtualdom"); + // let scope = unsafe { &mut *scope_id }; + + // scope.ensure_drop_safety(); + + todo!("drop the component's props"); + // 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. + scope + .items + .borrow_mut() + .listeners + .drain(..) + .map(|li| unsafe { &*li }) + .for_each(|listener| drop(listener.callback.borrow_mut().take())); + } + + pub(crate) fn run_scope(&self, id: &ScopeId) -> bool { + let scope = self + .get_scope(id) + .expect("The base scope should never be moved"); + + // 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(); + + // guarantee that we haven't screwed up - there should be no latent references anywhere + debug_assert!(items.listeners.is_empty()); + debug_assert!(items.suspended_nodes.is_empty()); + debug_assert!(items.borrowed_props.is_empty()); + + log::debug!("Borrowed stuff is successfully cleared"); + + // temporarily cast the vcomponent to the right lifetime + // let vcomp = scope.load_vcomp(); + } + + let render: &dyn Fn(&Scope) -> Element = unsafe { &*scope.caller }; + + // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. + scope.wip_frame().nodes.borrow_mut().clear(); + if let Some(key) = render(scope) { + dbg!(key); + + dbg!(&scope.wip_frame().nodes.borrow_mut()); + // let mut old = scope.old_root.borrow_mut(); + // let mut new = scope.new_root.borrow_mut(); + + // let new_old = new.clone(); + // *old = new_old; + // *new = Some(key); + + // dbg!(&old); + // dbg!(&new); + + // the user's component succeeded. We can safely cycle to the next frame + // scope.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; + scope.cycle_frame(); + + true + } else { + false + } + } + + pub fn wip_head(&self, id: &ScopeId) -> &VNode { + let scope = self.get_scope(id).unwrap(); + let wip_frame = scope.wip_frame(); + let nodes = wip_frame.nodes.borrow(); + let node = nodes.get(0).unwrap(); + unsafe { std::mem::transmute(node) } + // let root = scope.old_root.borrow(); + // let link = root.as_ref().unwrap(); + // dbg!(link); + + // // for now, components cannot pass portals through each other + // assert_eq!(link.scope_id, *id); + // // assert_eq!(link.gen_id, scope.generation.get() - 1); + + // // let items = scope.items.borrow(); + // let nodes = scope.wip_frame().nodes.borrow(); + // let node = nodes.get(link.link_idx).unwrap(); + // unsafe { std::mem::transmute(node) } + } + + pub fn fin_head(&self, id: &ScopeId) -> &VNode { + let scope = self.get_scope(id).unwrap(); + let wip_frame = scope.fin_frame(); + let nodes = wip_frame.nodes.borrow(); + let node = nodes.get(0).unwrap(); + unsafe { std::mem::transmute(node) } + // let scope = self.get_scope(id).unwrap(); + // let root = scope.new_root.borrow(); + // let link = root.as_ref().unwrap(); + + // // for now, components cannot pass portals through each other + // assert_eq!(link.scope_id, *id); + // // assert_eq!(link.gen_id, scope.generation.get()); + + // let nodes = scope.fin_frame().nodes.borrow(); + // let node = nodes.get(link.link_idx).unwrap(); + // unsafe { std::mem::transmute(node) } + } + pub fn root_node(&self, id: &ScopeId) -> &VNode { + self.wip_head(id) + } +} diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs deleted file mode 100644 index 95540d153..000000000 --- a/packages/core/src/tasks.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::innerlude::*; -use futures_channel::mpsc::UnboundedSender; - -pub struct TaskHandle { - pub(crate) sender: UnboundedSender, - pub(crate) our_id: u64, -} - -impl TaskHandle { - /// Toggles this coroutine off/on. - /// - /// This method is not synchronous - your task will not stop immediately. - pub fn toggle(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn resume(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ResumeTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn stop(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } - - /// This method is not synchronous - your task will not stop immediately. - pub fn restart(&self) { - self.sender - .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id))) - .unwrap() - } -} diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs index aab7f3373..e6795902e 100644 --- a/packages/core/src/test_dom.rs +++ b/packages/core/src/test_dom.rs @@ -6,15 +6,16 @@ use crate::nodes::IntoVNode; pub struct TestDom { bump: Bump, - scheduler: Scheduler, + // scheduler: Scheduler, } impl TestDom { pub fn new() -> TestDom { let bump = Bump::new(); let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let scheduler = Scheduler::new(sender, receiver); - TestDom { bump, scheduler } + todo!() + // let scheduler = Scheduler::new(sender, receiver, 10, 100); + // TestDom { bump, scheduler } } pub fn new_factory(&self) -> NodeFactory { @@ -32,7 +33,9 @@ impl TestDom { 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); + let mut machine: DiffState = todo!(); + // let mut machine = DiffState::new(mutations); + // let mut machine = DiffState::new(mutations); machine.stack.push(DiffInstruction::Diff { new, old }); machine.mutations } @@ -40,13 +43,17 @@ impl TestDom { pub fn create<'a>(&'a self, left: Option>) -> Mutations<'a> { let old = self.bump.alloc(self.render_direct(left)); - let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + let mut machine: DiffState = todo!(); + // let mut machine = DiffState::new(Mutations::new()); + // let mut machine = DiffState::new(Mutations::new()); machine.stack.create_node(old, MountType::Append); - machine.work(&mut || false); + todo!() - machine.mutations + // machine.work(&mut || false); + + // machine.mutations } pub fn lazy_diff<'a>( @@ -56,22 +63,26 @@ impl TestDom { ) -> (Mutations<'a>, Mutations<'a>) { let (old, new) = (self.render(left), self.render(right)); - let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + let mut machine: DiffState = todo!(); + // let mut machine = DiffState::new(Mutations::new()); machine.stack.create_node(old, MountType::Append); - machine.work(|| false); - let create_edits = machine.mutations; + todo!() - let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool); + // machine.work(|| false); + // let create_edits = machine.mutations; - machine.stack.push(DiffInstruction::Diff { old, new }); + // let mut machine: DiffState = todo!(); + // // let mut machine = DiffState::new(Mutations::new()); - machine.work(&mut || false); + // machine.stack.push(DiffInstruction::Diff { old, new }); - let edits = machine.mutations; + // machine.work(&mut || false); - (create_edits, edits) + // let edits = machine.mutations; + + // (create_edits, edits) } } diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 1ddd3a51c..b0da2c425 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -7,63 +7,6 @@ pub fn empty_cell() -> Cell> { Cell::new(None) } -pub fn type_name_of(_: T) -> &'static str { - std::any::type_name::() -} - -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; - -// use crate::task::{Context, Poll}; - -/// Cooperatively gives up a timeslice to the task scheduler. -/// -/// Calling this function will move the currently executing future to the back -/// of the execution queue, making room for other futures to execute. This is -/// especially useful after running CPU-intensive operations inside a future. -/// -/// See also [`task::spawn_blocking`]. -/// -/// [`task::spawn_blocking`]: fn.spawn_blocking.html -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # async_std::task::block_on(async { -/// # -/// use async_std::task; -/// -/// task::yield_now().await; -/// # -/// # }) -/// ``` -#[inline] -pub async fn yield_now() { - YieldNow(false).await -} - -struct YieldNow(bool); - -impl Future for YieldNow { - type Output = (); - - // The futures executor is implemented as a FIFO queue, so all this future - // does is re-schedule the future back to the end of the queue, giving room - // for other futures to progress. - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if !self.0 { - self.0 = true; - cx.waker().wake_by_ref(); - Poll::Pending - } else { - Poll::Ready(()) - } - } -} - /// A component's unique identifier. /// /// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6945520e4..6eb620d9e 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -19,10 +19,14 @@ //! 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. /// @@ -55,18 +59,22 @@ use std::{any::Any, rc::Rc}; /// } /// ``` pub struct VirtualDom { - scheduler: Scheduler, - base_scope: ScopeId, - root_fc: Box, + _root_caller: *mut dyn Fn(&Scope) -> Element, - root_props: Rc, + pub(crate) scopes: ScopeArena, - // we need to keep the allocation around, but we don't necessarily use it - _root_caller: Box, + receiver: UnboundedReceiver, + pub(crate) sender: UnboundedSender, + + // Every component that has futures that need to be polled + pending_futures: FxHashSet, + pending_messages: VecDeque, + dirty_scopes: IndexSet, } +// Methods to create the VirtualDom impl VirtualDom { /// Create a new VirtualDOM with a component that does not have special props. /// @@ -127,7 +135,7 @@ impl VirtualDom { 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,100 +145,283 @@ impl VirtualDom { sender: UnboundedSender, receiver: UnboundedReceiver, ) -> Self { - let root_fc = Box::new(root); + let mut scopes = ScopeArena::new(sender.clone()); - let root_props: Rc = Rc::new(root_props); - - let props = root_props.clone(); - - let root_caller: Box Element> = - Box::new(move |scope: &ScopeInner| { - let props = props.downcast_ref::

().unwrap(); - let node = root((scope, props)); - // cast into the right lifetime - unsafe { std::mem::transmute(node) } - }); - let caller = unsafe { bumpalo::boxed::Box::from_raw(root_caller.as_mut() as *mut _) }; - - let scheduler = Scheduler::new(sender, receiver); - - let base_scope = scheduler.pool.insert_scope_with_key(|myidx| { - ScopeInner::new(caller, myidx, None, 0, 0, scheduler.pool.channel.clone()) - }); + let caller = Box::new(move |f: &Scope| -> Element { root(f, &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); Self { - _root_caller: Box::new(root_caller), - root_fc, + scopes, base_scope, - scheduler, - root_props, + receiver, + // todo: clean this up manually? + _root_caller: caller_ref, + pending_messages: VecDeque::new(), + pending_futures: Default::default(), + dirty_scopes: Default::default(), + sender, } } - /// Get the [`Scope`] for the root component. + /// Get the [`ScopeState`] for the root component. /// /// 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) + /// Get the [`ScopeState`] for a component given its [`ScopeId`] + /// + /// # Example + /// + /// + /// + pub fn get_scope<'a>(&'a self, id: &ScopeId) -> Option<&'a Scope> { + self.scopes.get_scope(id) } - /// Update the root props of this VirtualDOM. + /// Get an [`UnboundedSender`] handle to the channel used by the scheduler. /// - /// 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 /// - /// ## Example /// - /// ```rust - /// #[derive(Props, PartialEq)] - /// struct AppProps { - /// route: &'static str - /// } - /// static App: FC = |(cx, props)|cx.render(rsx!{ "route is {cx.route}" }); + /// + pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender { + self.sender.clone() + } + + /// Check if the [`VirtualDom`] has any pending updates or work to be done. /// - /// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" }); + /// # Example /// - /// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap(); - /// ``` - pub fn update_root_props

(&mut self, root_props: P) -> Option - where - P: 'static, - { - let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); + /// + /// + pub fn has_any_work(&self) -> bool { + !(self.dirty_scopes.is_empty() && self.pending_messages.is_empty()) + } - // Pre-emptively drop any downstream references of the old props - root_scope.ensure_drop_safety(&self.scheduler.pool); + /// 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 - let mut root_props: Rc = Rc::new(root_props); - - if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { - // Swap the old props and new props - std::mem::swap(&mut self.root_props, &mut root_props); - - let root = *self.root_fc.downcast_ref::>().unwrap(); - - let root_caller: Box Element> = - Box::new(move |scope: &ScopeInner| unsafe { - let props: &'_ P = &*(props_ptr as *const P); - std::mem::transmute(root((scope, props))) - }); - - root_scope.update_scope_dependencies(&root_caller); - - drop(root_props); - - Some(self.rebuild()) + // if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed + if self.pending_futures.is_empty() { + self.pending_messages + .push_front(self.receiver.next().await.unwrap()); } else { - None + struct PollTasks<'a> { + pending_futures: &'a FxHashSet, + scopes: &'a ScopeArena, + } + + impl<'a> Future for PollTasks<'a> { + type Output = (); + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll { + let mut all_pending = true; + + // Poll every scope manually + for fut in self.pending_futures.iter() { + let scope = self + .scopes + .get_scope(fut) + .expect("Scope should never be moved"); + + let mut items = scope.items.borrow_mut(); + for task in items.tasks.iter_mut() { + let task = task.as_mut(); + + // todo: does this make sense? + // I don't usually write futures by hand + // I think the futures neeed to be pinned using bumpbox or something + // right now, they're bump allocated so this shouldn't matter anyway - they're not going to move + let unpinned = unsafe { Pin::new_unchecked(task) }; + + if unpinned.poll(cx).is_ready() { + all_pending = false + } + } + } + + // Resolve the future if any singular task is ready + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + } + } + + // Poll both the futures and the scheduler message queue simulataneously + use futures_util::future::{select, Either}; + + let scheduler_fut = self.receiver.next(); + let tasks_fut = PollTasks { + pending_futures: &self.pending_futures, + scopes: &self.scopes, + }; + + match select(tasks_fut, scheduler_fut).await { + // Futures don't generate work + Either::Left((_, _)) => {} + + // Save these messages in FIFO to be processed later + Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), + } } } + /// Run the virtualdom with a deadline. + /// + /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, + /// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will + /// exhaust the deadline working on them. + /// + /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent + /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. + /// + /// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline + /// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the + /// deadline closure manually. + /// + /// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking + /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then + /// the screen will "jank" up. In debug, this will trigger an alert. + /// + /// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if + /// the provided deadline future resolves. + /// + /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the + /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft + /// entirely jank-free applications that perform a ton of work. + /// + /// # Example + /// + /// ```no_run + /// static App: FC<()> = |(cx, props)|rsx!(cx, div {"hello"} ); + /// + /// let mut dom = VirtualDom::new(App); + /// + /// loop { + /// 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); + /// } + /// ``` + /// + /// ## Mutations + /// + /// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also + /// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has + /// applied the edits. + /// + /// Mutations are the only link between the RealDOM and the VirtualDOM. + pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { + let mut committed_mutations = vec![]; + + while self.has_any_work() { + while let Ok(Some(msg)) = self.receiver.try_next() { + self.pending_messages.push_front(msg); + } + + 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); + + let scope = self.scopes.get_scope(&event.scope_id).unwrap(); + + // 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); + } + } + } + } + + let mut diff_state: DiffState = DiffState::new(Mutations::new()); + + 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 }); + } + } + } + + let work_completed = self.scopes.work(&mut diff_state, &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(), 0); + + 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. @@ -242,14 +433,27 @@ impl VirtualDom { /// /// # Example /// ``` - /// static App: FC<()> = |(cx, props)|cx.render(rsx!{ "hello world" }); + /// 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) + let mut diff_machine = DiffState::new(Mutations::new()); + + let scope_id = self.base_scope; + if self.scopes.run_scope(&scope_id) { + diff_machine + .stack + .create_node(self.scopes.fin_head(&scope_id), MountType::Append); + + diff_machine.stack.scope_stack.push(scope_id); + + self.scopes.work(&mut diff_machine, || false); + } + + diff_machine.mutations } /// Compute a manual diff of the VirtualDOM between states. @@ -286,107 +490,100 @@ impl VirtualDom { /// /// let edits = dom.diff(); /// ``` - pub fn diff(&mut self) -> Mutations { - self.scheduler.hard_diff(self.base_scope) - } + pub fn hard_diff<'a>(&'a mut self, scope_id: &ScopeId) -> Option> { + log::debug!("hard diff {:?}", scope_id); - /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. - /// - /// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will - /// return "None" - pub fn run_immediate(&mut self) -> Option> { - if self.scheduler.has_any_work() { - Some(self.scheduler.work_sync()) + if self.scopes.run_scope(scope_id) { + let mut diff_machine = DiffState::new(Mutations::new()); + + diff_machine.force_diff = true; + + self.scopes.diff_scope(&mut diff_machine, scope_id); + + dbg!(&diff_machine.mutations); + + Some(diff_machine.mutations) } else { None } } +} - /// Run the virtualdom with a deadline. +pub enum SchedulerMsg { + // events from the host + UiEvent(UserEvent), + + // setstate + Immediate(ScopeId), +} + +#[derive(Debug)] +pub struct UserEvent { + /// The originator of the event trigger + pub scope_id: ScopeId, + + pub priority: EventPriority, + + /// The optional real node associated with the trigger + pub mounted_dom_id: Option, + + /// The event type IE "onclick" or "onmouseover" /// - /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline, - /// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will - /// exhaust the deadline working on them. + /// The name that the renderer will use to mount the listener. + pub name: &'static str, + + /// Event Data + pub event: Box, +} + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// Work that must be completed during the EventHandler phase. /// - /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent - /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. + /// 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. /// - /// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline - /// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the - /// deadline closure manually. + /// This is typically reserved for things like user interaction. /// - /// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking - /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then - /// the screen will "jank" up. In debug, this will trigger an alert. + /// 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. /// - /// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if - /// the provided deadline future resolves. + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. /// - /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the - /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft - /// entirely jank-free applications that perform a ton of work. + /// 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. /// - /// # Example + /// The primary user of Low Priority work is the asynchronous work system (Suspense). /// - /// ```no_run - /// static App: FC<()> = |(cx, props)|rsx!(cx, div {"hello"} ); - /// let mut dom = VirtualDom::new(App); - /// loop { - /// let deadline = TimeoutFuture::from_ms(16); - /// let mutations = dom.run_with_deadline(deadline).await; - /// apply_mutations(mutations); - /// } - /// ``` - /// - /// ## Mutations - /// - /// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also - /// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has - /// applied the edits. - /// - /// Mutations are the only link between the RealDOM and the VirtualDOM. - pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec> { - self.scheduler.work_with_deadline(deadline) - } - - pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { - self.scheduler.pool.channel.sender.clone() - } - - /// Waits for the scheduler to have work - /// This lets us poll async tasks during idle periods without blocking the main thread. - pub async fn wait_for_work(&mut self) { - // todo: poll the events once even if there is work to do to prevent starvation - if self.scheduler.has_any_work() { - return; - } - - use futures_util::StreamExt; - - // Wait for any new events if we have nothing to do - - let tasks_fut = self.scheduler.async_tasks.next(); - let scheduler_fut = self.scheduler.receiver.next(); - - use futures_util::future::{select, Either}; - match select(tasks_fut, scheduler_fut).await { - // poll the internal futures - Either::Left((_id, _)) => { - // - } - - // wait for an external event - Either::Right((msg, _)) => match msg.unwrap() { - SchedulerMsg::Task(t) => { - self.scheduler.handle_task(t); - } - SchedulerMsg::Immediate(im) => { - self.scheduler.dirty_scopes.insert(im); - } - SchedulerMsg::UiEvent(evt) => { - self.scheduler.ui_events.push_back(evt); - } - }, - } - } + /// This is considered "idle" work or "background" work. + Low = 0, } diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index c46a8fe3c..692a8f8db 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -10,8 +10,8 @@ fn test_borrowed_state() { let _ = VirtualDom::new(Parent); } -fn Parent((cx, _): Scope<()>) -> Element { - let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {}); +fn Parent(cx: Context, props: &()) -> Element { + let value = cx.use_hook(|_| String::new(), |f| &*f); cx.render(rsx! { div { @@ -28,7 +28,7 @@ struct ChildProps<'a> { name: &'a str, } -fn Child<'a>((cx, props): Scope<'a, ChildProps>) -> Element<'a> { +fn Child(cx: Context, props: &ChildProps) -> Element { cx.render(rsx! { div { h1 { "it's nested" } @@ -42,7 +42,7 @@ struct Grandchild<'a> { name: &'a str, } -fn Child2<'a>((cx, props): Scope<'a, Grandchild>) -> Element<'a> { +fn Child2(cx: Context, props: &Grandchild) -> Element { cx.render(rsx! { div { "Hello {props.name}!" } }) diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 712bf3713..00e3debec 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -21,7 +21,7 @@ fn new_dom(app: FC

, props: P) -> VirtualDom { #[test] fn test_original_diff() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { @@ -57,17 +57,17 @@ fn test_original_diff() { #[test] fn create() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div { div { "Hello, world!" div { div { - // Fragment { - // "hello" - // "world" - // } + Fragment { + "hello" + "world" + } } } } @@ -120,7 +120,7 @@ fn create() { #[test] fn create_list() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { {(0..3).map(|f| rsx!{ div { "hello" @@ -169,7 +169,7 @@ fn create_list() { #[test] fn create_simple() { - static APP: FC<()> = |(cx, props)| { + static APP: FC<()> = |cx, props| { cx.render(rsx! { div {} div {} @@ -207,7 +207,7 @@ fn create_simple() { } #[test] fn create_components() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx! { Child { "abc1" } Child { "abc2" } @@ -220,7 +220,7 @@ fn create_components() { children: ScopeChildren<'a>, } - fn Child<'a>((cx, props): Scope<'a, ChildProps<'a>>) -> Element { + fn Child<'a>(cx: Context<'a>, props: &ChildProps<'a>) -> 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" } })} @@ -302,17 +302,18 @@ fn anchors() { #[test] fn suspended() { - static App: FC<()> = |(cx, props)| { - let val = use_suspense(cx, || async {}, |cx, p| todo!()); + todo!() + // static App: FC<()> = |cx, props| { + // let val = use_suspense(cx, || async {}, |p| todo!()); - cx.render(rsx! { {val} }) - }; + // cx.render(rsx! { {val} }) + // }; - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); + // let mut dom = new_dom(App, ()); + // let mutations = dom.rebuild(); - assert_eq!( - mutations.edits, - [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 },] - ); + // assert_eq!( + // mutations.edits, + // [CreatePlaceholder { root: 0 }, AppendChildren { many: 1 },] + // ); } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 65aece563..93e7bd397 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -5,7 +5,7 @@ //! //! 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, TestDom, VSuspended}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/display_vdom.rs b/packages/core/tests/display_vdom.rs index c7acc712c..ee32f27c0 100644 --- a/packages/core/tests/display_vdom.rs +++ b/packages/core/tests/display_vdom.rs @@ -13,7 +13,7 @@ mod test_logging; #[test] fn please_work() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx! { div { hidden: "true" @@ -27,7 +27,7 @@ fn please_work() { }) }; - static Child: FC<()> = |(cx, props)| { + static Child: FC<()> = |cx, props| { cx.render(rsx! { div { "child" } }) @@ -35,6 +35,4 @@ fn please_work() { let mut dom = VirtualDom::new(App); dom.rebuild(); - - println!("{}", dom); } diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 3b9c05cca..1e65f8f58 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -20,7 +20,7 @@ fn manual_diffing() { value: Shared<&'static str>, } - static App: FC = |(cx, props)| { + static App: FC = |cx, props| { let val = props.value.lock().unwrap(); cx.render(rsx! { div { "{val}" } }) }; @@ -37,7 +37,7 @@ fn manual_diffing() { *value.lock().unwrap() = "goodbye"; - let edits = dom.diff(); + let edits = dom.rebuild(); log::debug!("edits: {:?}", edits); } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 7bb9e6501..5a2e8d042 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -13,12 +13,12 @@ mod test_logging; fn shared_state_test() { struct MySharedState(&'static str); - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.provide_state(MySharedState("world!")); cx.render(rsx!(Child {})) }; - static Child: FC<()> = |(cx, props)| { + static Child: FC<()> = |cx, props| { let shared = cx.consume_state::()?; cx.render(rsx!("Hello, {shared.0}")) }; diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 79aaad112..9aa805918 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -17,7 +17,7 @@ use dioxus_html as dioxus_elements; #[test] fn app_runs() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { // cx.render(rsx!( div{"hello"} )) }; @@ -28,7 +28,7 @@ fn app_runs() { #[test] fn fragments_work() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( div{"hello"} div{"goodbye"} @@ -42,7 +42,7 @@ fn fragments_work() { #[test] fn lists_work() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {(0..6).map(|f| rsx!(span{ "{f}" }))} @@ -55,7 +55,7 @@ fn lists_work() { #[test] fn conditional_rendering() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} {true.then(|| rsx!(span{ "a" }))} @@ -72,13 +72,13 @@ fn conditional_rendering() { #[test] fn child_components() { - static App: FC<()> = |(cx, props)| { + static App: FC<()> = |cx, props| { cx.render(rsx!( {true.then(|| rsx!(Child { }))} {false.then(|| rsx!(Child { }))} )) }; - static Child: FC<()> = |(cx, props)| { + static Child: FC<()> = |cx, props| { cx.render(rsx!( h1 {"hello"} h1 {"goodbye"} @@ -91,13 +91,14 @@ fn child_components() { #[test] fn suspended_works() { - static App: FC<()> = |(cx, props)| { - let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!()); - // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"}); - cx.render(rsx!("hello" { title })) - }; + todo!() + // static App: FC<()> = |cx, props| { + // let title = use_suspense(cx, || async { "bob" }, move |cx, f| todo!()); + // // let title = use_suspense(cx, || async { "bob" }, move |cx, f| rsx! { "{f}"}); + // cx.render(rsx!("hello" { title })) + // }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); + // let mut vdom = VirtualDom::new(App); + // let edits = vdom.rebuild(); + // dbg!(edits); } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e6aef1378..142b76367 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -24,6 +24,7 @@ tokio = { version = "1.12.0", features = [ "rt", ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro" } +dioxus-html = { path = "../html", features = ["serialize"] } [features] default = ["tokio_runtime"] @@ -31,6 +32,5 @@ tokio_runtime = ["tokio"] [dev-dependencies] -dioxus-html = { path = "../html" } dioxus-hooks = { path = "../hooks" } simple_logger = "1.13.0" diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 54b62217a..1f5135d8d 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -57,7 +57,7 @@ pub struct WebviewWindowProps<'a> { /// /// /// -pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element<'a> { +pub fn WebviewWindow((cx, props): Scope) -> Element { let dtcx = cx.consume_state::>()?; cx.use_hook( @@ -67,9 +67,6 @@ pub fn WebviewWindow<'a>((cx, props): Scope<'a, WebviewWindowProps>) -> Element< |state| { // }, - |hook| { - // - }, ); // render the children directly diff --git a/packages/desktop/src/err.rs b/packages/desktop/src/err.rs index e69de29bb..8b1378917 100644 --- a/packages/desktop/src/err.rs +++ b/packages/desktop/src/err.rs @@ -0,0 +1 @@ + diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 82a698f05..377964b7c 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -4,7 +4,8 @@ use std::sync::Arc; use std::{any::Any, rc::Rc}; -use dioxus_core::{events::on::MouseEvent, ElementId, EventPriority, ScopeId, UserEvent}; +use dioxus_core::{ElementId, EventPriority, ScopeId, UserEvent}; +use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] struct ImEvent { @@ -23,7 +24,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { contents, } = ims.into_iter().next().unwrap(); - let scope = ScopeId(scope as usize); + let scope_id = ScopeId(scope as usize); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); let name = event_name_from_typ(&event); @@ -31,15 +32,14 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { UserEvent { name, - event, - scope, + priority: EventPriority::Low, + scope_id, mounted_dom_id, + event, } } fn make_synthetic_event(name: &str, val: serde_json::Value) -> Box { - use dioxus_core::events::on::*; - match name { "copy" | "cut" | "paste" => { // diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 7484f1bbc..a7160fa8e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,7 +13,6 @@ use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; use cfg::DesktopConfig; -use dioxus_core::scheduler::SchedulerMsg; use dioxus_core::*; use serde::{Deserialize, Serialize}; diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index b1febb498..f9856eeaa 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -82,13 +82,6 @@ pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option None, } }, - |f| { - // we need to unsubscribe when our component is unounted - if let Some(root) = &f.root { - let mut root = root.borrow_mut(); - root.consumers.remove(&f.scope_id); - } - }, ) } @@ -98,6 +91,15 @@ struct SharedStateInner { scope_id: ScopeId, needs_notification: Cell, } +impl Drop for SharedStateInner { + fn drop(&mut self) { + // we need to unsubscribe when our component is unounted + if let Some(root) = &self.root { + let mut root = root.borrow_mut(); + root.consumers.remove(&self.scope_id); + } + } +} pub struct UseSharedState<'a, T: 'static> { pub(crate) cx: Context<'a>, @@ -172,6 +174,5 @@ pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T) cx.provide_state(state) }, |inner| {}, - |_| {}, ) } diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 0f780b6e4..75ae9c82b 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -16,7 +16,6 @@ pub fn use_ref(cx: Context, f: impl FnOnce() -> T) -> UseRef { inner.update_scheuled.set(false); UseRef { inner } }, - |_| {}, ) } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 76a72ea75..1989fcd20 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -69,7 +69,6 @@ pub fn use_state<'a, T: 'static>( UseState { inner: &*hook } }, - |_| {}, ) } struct UseStateInner { diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 20ef160d4..4903276e9 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -9,7 +9,12 @@ description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Vir # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version = "0.1.2" } +dioxus-core = { path = "../core", version = "0.1.3" } +# Serialize the Edits for use in Webview/Liveview instances +serde = { version = "1", features = ["derive"], optional = true } +serde_repr = { version = "0.1.7", optional = true } -[dev-dependencies] -scraper = "0.12.0" + +[features] +default = [] +serialize = ["serde", "serde_repr"] diff --git a/packages/html/src/attrval.rs b/packages/html/src/attrval.rs deleted file mode 100644 index 9ac1f1373..000000000 --- a/packages/html/src/attrval.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! This module is not included anywhere. -//! -//! It is a prototype for a system that supports non-string attribute values. - -trait AsAttributeValue: Sized { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a>; -} -enum AttributeValue<'a> { - Int(i32), - Float(f32), - Str(&'a str), - Bool(bool), -} -impl<'b> AsAttributeValue for Arguments<'b> { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for &'static str { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for f32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} -impl AsAttributeValue for i32 { - fn into_attribute_value<'a>(self, cx: NodeFactory<'a>) -> AttributeValue<'a> { - todo!() - } -} diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs new file mode 100644 index 000000000..6406e54c8 --- /dev/null +++ b/packages/html/src/elements.rs @@ -0,0 +1,1126 @@ +use crate::{GlobalAttributes, SvgAttributes}; +use dioxus_core::*; +use std::fmt::Arguments; + +macro_rules! builder_constructors { + ( + $( + $(#[$attr:meta])* + $name:ident { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* + }; + )* + ) => { + $( + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl DioxusElement for $name { + const TAG_NAME: &'static str = stringify!($name); + const NAME_SPACE: Option<&'static str> = None; + } + + impl GlobalAttributes for $name {} + + impl $name { + $( + $(#[$attr_method])* + pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr(stringify!($fil), val, None, false) + } + )* + } + )* + }; + + ( $( + $(#[$attr:meta])* + $name:ident <> $namespace:tt { + $($fil:ident: $vil:ident,)* + }; + )* ) => { + $( + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl DioxusElement for $name { + const TAG_NAME: &'static str = stringify!($name); + const NAME_SPACE: Option<&'static str> = Some($namespace); + } + + impl SvgAttributes for $name {} + + impl $name { + $( + pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr(stringify!($fil), val, Some(stringify!($namespace)), false) + } + )* + } + )* + }; +} + +// Organized in the same order as +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +// +// Does not include obsolete elements. +// +// This namespace represents a collection of modern HTML-5 compatiable elements. +// +// This list does not include obsolete, deprecated, experimental, or poorly supported elements. +builder_constructors! { + // Document metadata + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) + /// element. + /// + base { + href: Uri, + target: Target, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) + /// element. + head {}; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + /// element. + link { + // as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + r#type: Mime, + integrity: String, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) + /// element. + meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: HTTPEquiv, + name: Metadata, + }; + + /// Build a + /// [`