From b56ea6c9a99cd747f376aca4a3c439c13f082714 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 3 Nov 2021 19:55:02 -0400 Subject: [PATCH] wip: work on scheduler, async, coroutines, and merge scope into context --- examples/async.rs | 2 +- examples/coroutine.rs | 4 +- packages/core/src/context.rs | 271 ----------------------------- packages/core/src/coroutines.rs | 17 ++ packages/core/src/hooks.rs | 175 +++++++++---------- packages/core/src/lib.rs | 49 +++--- packages/core/src/nodes.rs | 27 ++- packages/core/src/scheduler.rs | 4 + packages/core/src/scope.rs | 283 +++++++++++++++++++++++++++++-- packages/core/src/virtual_dom.rs | 29 ++-- packages/core/tests/task.rs | 1 + 11 files changed, 420 insertions(+), 442 deletions(-) delete mode 100644 packages/core/src/context.rs create mode 100644 packages/core/src/coroutines.rs create mode 100644 packages/core/tests/task.rs diff --git a/examples/async.rs b/examples/async.rs index c9abce7c2..b0ea0a7b4 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -17,7 +17,7 @@ pub static App: FC<()> = |(cx, _)| { let (async_count, dir) = (count.for_async(), *direction); - let (task, _) = use_task(cx, move || async move { + let (task, _) = use_coroutine(cx, move || async move { loop { TimeoutFuture::new(250).await; *async_count.get_mut() += dir; diff --git a/examples/coroutine.rs b/examples/coroutine.rs index 3cafeb055..61b065022 100644 --- a/examples/coroutine.rs +++ b/examples/coroutine.rs @@ -32,13 +32,13 @@ static App: FC<()> = |(cx, props)| { let p2 = use_state(cx, || 0); let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async()); - let (p1_handle, _) = use_task(cx, || async move { + let (p1_handle, _) = use_coroutine(cx, || async move { loop { *p1_async.get_mut() += 1; async_std::task::sleep(std::time::Duration::from_millis(75)).await; } }); - let (p2_handle, _) = use_task(cx, || async move { + let (p2_handle, _) = use_coroutine(cx, || async move { loop { *p2_async.get_mut() += 1; async_std::task::sleep(std::time::Duration::from_millis(100)).await; diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs deleted file mode 100644 index 95953ff16..000000000 --- a/packages/core/src/context.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Public APIs for managing component state, tasks, and lifecycles. -//! -//! This module is separate from `Scope` to narrow what exactly is exposed to user code. -//! -//! We unsafely implement `send` for the VirtualDom, but those guarantees can only be - -use bumpalo::Bump; - -use crate::{innerlude::*, lazynodes::LazyNodes}; -use std::{any::TypeId, ops::Deref, rc::Rc}; - -/// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// -/// This lets components access props, schedule updates, integrate hooks, and expose shared state. -/// -/// Note: all of these methods are *imperative* - they do not act as hooks! They are meant to be used by hooks -/// to provide complex behavior. For instance, calling "add_shared_state" on every render is considered a leak. This method -/// exists for the `use_provide_state` hook to provide a shared state object. -/// -/// For the most part, the only method you should be using regularly is `render`. -/// -/// ## Example -/// -/// ```ignore -/// #[derive(Properties)] -/// struct Props { -/// name: String -/// } -/// -/// fn example(cx: Context) -> VNode { -/// html! { -///
"Hello, {cx.name}"
-/// } -/// } -/// ``` -pub struct Context<'src> { - pub scope: &'src ScopeInner, -} - -impl<'src> Copy for Context<'src> {} -impl<'src> Clone for Context<'src> { - fn clone(&self) -> Self { - Self { scope: self.scope } - } -} - -// We currently deref to props, but it might make more sense to deref to Scope? -// This allows for code that takes cx.xyz instead of cx.props.xyz -impl<'a> Deref for Context<'a> { - type Target = &'a ScopeInner; - fn deref(&self) -> &Self::Target { - &self.scope - } -} - -impl<'src> Context<'src> { - /// Create a subscription that schedules a future render for the reference component - /// - /// ## Notice: you should prefer using prepare_update and get_scope_id - pub fn schedule_update(&self) -> Rc { - self.scope.memoized_updater.clone() - } - - pub fn needs_update(&self) { - (self.scope.memoized_updater)() - } - - pub fn needs_update_any(&self, id: ScopeId) { - (self.scope.shared.schedule_any_immediate)(id) - } - - /// Schedule an update for any component given its ScopeId. - /// - /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method. - /// - /// This method should be used when you want to schedule an update for a component - pub fn schedule_update_any(&self) -> Rc { - self.scope.shared.schedule_any_immediate.clone() - } - - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. - pub fn scope_id(&self) -> ScopeId { - self.scope.our_arena_idx - } - - pub fn bump(&self) -> &'src Bump { - let bump = &self.scope.frames.wip_frame().bump; - bump - } - - /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. - /// - /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. - /// - /// ## Example - /// - /// ```ignore - /// fn Component(cx: Context<()>) -> VNode { - /// // Lazy assemble the VNode tree - /// let lazy_tree = html! {
"Hello World"
}; - /// - /// // Actually build the tree and allocate it - /// cx.render(lazy_tree) - /// } - ///``` - pub fn render(self, lazy_nodes: Option>) -> Option> { - let bump = &self.scope.frames.wip_frame().bump; - let factory = NodeFactory { bump }; - lazy_nodes.map(|f| f.call(factory)) - } - - /// `submit_task` will submit the future to be polled. - /// - /// This is useful when you have some async task that needs to be progressed. - /// - /// This method takes ownership over the task you've provided, and must return (). This means any work that needs to - /// happen must occur within the future or scheduled for after the future completes (through schedule_update ) - /// - /// ## Explanation - /// Dioxus will step its internal event loop if the future returns if the future completes while waiting. - /// - /// Tasks can't return anything, but they can be controlled with the returned handle - /// - /// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it - /// is considered "stable" - /// - /// - /// - pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - (self.scope.shared.submit_task)(task) - } - - /// This method enables the ability to expose state to children further down the VirtualDOM Tree. - /// - /// This is a "fundamental" operation and should only be called during initialization of a hook. - /// - /// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead. - /// - /// When the component is dropped, so is the context. Be aware of this behavior when consuming - /// the context via Rc/Weak. - /// - /// # Example - /// - /// ``` - /// struct SharedState(&'static str); - /// - /// static App: FC<()> = |(cx, props)|{ - /// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {}); - /// rsx!(cx, Child {}) - /// } - /// - /// static Child: FC<()> = |(cx, props)|{ - /// let state = cx.consume_state::(); - /// rsx!(cx, div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_state(self, value: T) - where - T: 'static, - { - self.scope - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Rc::new(value)) - .map(|f| f.downcast::().ok()) - .flatten(); - } - - /// Try to retrieve a SharedState with type T from the any parent Scope. - pub fn consume_state(self) -> Option> { - let getter = &self.scope.shared.get_shared_context; - let ty = TypeId::of::(); - let idx = self.scope.our_arena_idx; - getter(idx, ty).map(|f| f.downcast().unwrap()) - } - - /// Create a new subtree with this scope as the root of the subtree. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// This method - /// - /// # Example - /// - /// ```rust - /// static App: FC<()> = |(cx, props)| { - /// todo!(); - /// rsx!(cx, div { "Subtree {id}"}) - /// }; - /// ``` - pub fn create_subtree(self) -> Option { - self.scope.new_subtree() - } - - /// Get the subtree ID that this scope belongs to. - /// - /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route - /// the mutations to the correct window/portal/subtree. - /// - /// # Example - /// - /// ```rust - /// static App: FC<()> = |(cx, props)| { - /// let id = cx.get_current_subtree(); - /// rsx!(cx, div { "Subtree {id}"}) - /// }; - /// ``` - pub fn get_current_subtree(self) -> u32 { - self.scope.subtree() - } - - /// Store a value between renders - /// - /// This is *the* foundational hook for all other hooks. - /// - /// - Initializer: closure used to create the initial hook state - /// - Runner: closure used to output a value every time the hook is used - /// - Cleanup: closure used to teardown the hook once the dom is cleaned up - /// - /// - /// # Example - /// - /// ```ignore - /// // use_ref is the simplest way of storing a value between renders - /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell { - /// use_hook( - /// || Rc::new(RefCell::new(initial_value())), - /// |state| state, - /// |_| {}, - /// ) - /// } - /// ``` - pub fn use_hook( - self, - initializer: Init, - runner: Run, - cleanup: Cleanup, - ) -> Output - where - State: 'static, - Output: 'src, - Init: FnOnce(usize) -> State, - Run: FnOnce(&'src mut State) -> Output, - Cleanup: FnOnce(Box) + 'static, - { - // If the idx is the same as the hook length, then we need to add the current hook - if self.scope.hooks.at_end() { - self.scope.hooks.push_hook( - initializer(self.scope.hooks.len()), - Box::new(|raw| { - // - let s = raw.downcast::().unwrap(); - cleanup(s); - }), - ); - } - - runner(self.scope.hooks.next::().expect(HOOK_ERR_MSG)) - } -} - -const HOOK_ERR_MSG: &str = r###" -Unable to retrieve the hook that was initialized at this index. -Consult the `rules of hooks` to understand how to use hooks properly. - -You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. -Functions prefixed with "use" should never be called conditionally. -"###; diff --git a/packages/core/src/coroutines.rs b/packages/core/src/coroutines.rs new file mode 100644 index 000000000..490a0acab --- /dev/null +++ b/packages/core/src/coroutines.rs @@ -0,0 +1,17 @@ +//! 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/hooks.rs b/packages/core/src/hooks.rs index 1c52a3422..b18a445c1 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -30,7 +30,7 @@ use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc}; /// } /// }; /// ``` -pub fn use_task<'src, Out, Fut, Init>( +pub fn use_coroutine<'src, Out, Fut, Init>( cx: Context<'src>, task_initializer: Init, ) -> (&'src TaskHandle, &'src Option) @@ -45,38 +45,40 @@ where value: Option, } - // whenever the task is complete, save it into th - cx.use_hook( - move |_| { - let task_fut = task_initializer(); + todo!() - let task_dump = Rc::new(RefCell::new(None)); + // // whenever the task is complete, save it into th + // cx.use_hook( + // move |_| { + // let task_fut = task_initializer(); - let slot = task_dump.clone(); + // let task_dump = Rc::new(RefCell::new(None)); - let updater = cx.schedule_update_any(); - let originator = cx.scope.our_arena_idx; + // let slot = task_dump.clone(); - 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 updater = cx.schedule_update_any(); + // let originator = cx.scope.our_arena_idx; - 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) - }, - |_| {}, - ) + // let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move { + // *slot.as_ref().borrow_mut() = Some(output); + // updater(originator); + // originator + // }))); + + // TaskHook { + // task_dump, + // value: None, + // handle, + // } + // }, + // |hook| { + // if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { + // hook.value = Some(val); + // } + // (&hook.handle, &hook.value) + // }, + // |_| {}, + // ) } /// Asynchronously render new nodes once the given future has completed. @@ -95,9 +97,9 @@ pub fn use_suspense<'src, Out, Fut, Cb>( user_callback: Cb, ) -> Element<'src> where - Fut: Future + 'static, + Fut: Future, Out: 'static, - Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> Element<'a> + 'static, + Cb: FnMut(&Out) -> Element<'src> + 'src, { /* General strategy: @@ -109,65 +111,60 @@ where - if it does, then we can render the node directly - if it doesn't, then we render a suspended node along with with the callback and task id */ - cx.use_hook( - move |_| { - let value = Rc::new(RefCell::new(None)); - let slot = value.clone(); - let originator = cx.scope.our_arena_idx; + todo!() + // cx.use_hook( + // move |_| { + // let value = Rc::new(RefCell::new(None)); + // let slot = value.clone(); + // let originator = cx.scope.our_arena_idx; - let handle = cx.submit_task(Box::pin(task_initializer().then( - move |output| async move { - *slot.borrow_mut() = Some(Box::new(output) as Box); - originator - }, - ))); + // let handle = cx.submit_task(Box::pin(task_initializer().then( + // move |output| async move { + // *slot.borrow_mut() = Some(Box::new(output) as Box); + // originator + // }, + // ))); - SuspenseHook { handle, value } - }, - move |hook| match hook.value.borrow().as_ref() { - Some(value) => { - let out = value.downcast_ref::().unwrap(); - let sus = SuspendedContext { - inner: Context { scope: cx.scope }, - }; - user_callback(sus, out) - } - None => { - let value = hook.value.clone(); + // SuspenseHook { handle, value } + // }, + // move |hook| { + // // If the value exists, just run the callback to get the contents + // // if the value doesn't exist, we want to render a suspended node with an associated callback + // if let Some(value) = hook.value.borrow().as_ref() { + // let out = value.downcast_ref::().unwrap(); + // user_callback(out) + // } else { + // let value = hook.value.clone(); - let id = hook.handle.our_id; + // let id = hook.handle.our_id; - todo!() - // Some(LazyNodes::new(move |f| { - // let bump = f.bump(); + // let bump = cx.bump(); - // use bumpalo::boxed::Box as BumpBox; + // use bumpalo::boxed::Box as BumpBox; - // let f: &mut dyn FnMut(SuspendedContext<'src>) -> Element<'src> = - // bump.alloc(move |sus| { - // let val = value.borrow(); + // let f: &mut dyn FnMut() -> Element<'src> = bump.alloc(move || { + // let val = value.borrow(); - // let out = val - // .as_ref() - // .unwrap() - // .as_ref() - // .downcast_ref::() - // .unwrap(); + // let out = val + // .as_ref() + // .unwrap() + // .as_ref() + // .downcast_ref::() + // .unwrap(); - // user_callback(sus, out) - // }); - // let callback = unsafe { BumpBox::from_raw(f) }; + // user_callback(out) + // }); + // let callback = unsafe { BumpBox::from_raw(f) }; - // VNode::Suspended(bump.alloc(VSuspended { - // dom_id: empty_cell(), - // task_id: id, - // callback: RefCell::new(Some(callback)), - // })) - // })) - } - }, - |_| {}, - ) + // Some(VNode::Suspended(bump.alloc(VSuspended { + // dom_id: empty_cell(), + // task_id: id, + // callback: RefCell::new(Some(callback)), + // }))) + // } + // }, + // |_| {}, + // ) } pub(crate) struct SuspenseHook { @@ -175,24 +172,6 @@ pub(crate) struct SuspenseHook { pub value: Rc>>>, } -pub struct SuspendedContext<'a> { - pub(crate) inner: Context<'a>, -} - -impl<'src> SuspendedContext<'src> { - // // pub fn render( - // pub fn render( - // // pub fn render) -> VNode<'src>>( - // self, - // lazy_nodes: LazyNodes<'_>, - // // lazy_nodes: LazyNodes<'src, '_>, - // ) -> Element<'src> { - // let bump = &self.inner.scope.frames.wip_frame().bump; - // todo!("suspense") - // // Some(lazy_nodes.into_vnode(NodeFactory { bump })) - // } -} - #[derive(Clone, Copy)] pub struct NodeRef<'src, T: 'static>(&'src RefCell>); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f602f512e..d023b2c37 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,27 +12,27 @@ Navigating this crate: Some utilities */ -pub mod bumpframe; -pub mod childiter; -pub mod component; -pub mod context; -pub mod diff; -pub mod diff_stack; -pub mod events; -pub mod heuristics; -pub mod hooklist; -pub mod hooks; -pub mod lazynodes; -pub mod mutations; -pub mod nodes; -pub mod resources; -pub mod scheduler; -pub mod scope; -pub mod tasks; -pub mod test_dom; -pub mod threadsafe; -pub mod util; -pub mod virtual_dom; +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 test_dom; +pub(crate) mod threadsafe; +pub(crate) mod util; +pub(crate) mod virtual_dom; #[cfg(feature = "debug_vdom")] pub mod debug_dom; @@ -41,7 +41,6 @@ pub(crate) mod innerlude { pub(crate) use crate::bumpframe::*; pub(crate) use crate::childiter::*; pub use crate::component::*; - pub use crate::context::*; pub(crate) use crate::diff::*; pub use crate::diff_stack::*; pub use crate::events::*; @@ -66,14 +65,14 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, LazyNodes, MountType, - Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, SuspendedContext, - TaskHandle, TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, + Mutations, NodeFactory, Properties, SchedulerMsg, ScopeChildren, ScopeId, TaskHandle, TestDom, + ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC, }; pub mod prelude { pub use crate::component::{fc_to_builder, Fragment, Properties, Scope}; - pub use crate::context::Context; pub use crate::hooks::*; + pub use crate::innerlude::Context; pub use crate::innerlude::{DioxusElement, Element, LazyNodes, NodeFactory, 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 9b9693a8e..1cb06a4d6 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,14 +4,12 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{ - empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner, - SuspendedContext, - }, + innerlude::{empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId, ScopeInner}, lazynodes::LazyNodes, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ + any::Any, cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, }; @@ -307,10 +305,8 @@ pub struct Listener<'bump> { /// IE "click" - whatever the renderer needs to attach the listener by name. pub event: &'static str, - #[allow(clippy::type_complexity)] /// The actual callback that the user specified - pub(crate) callback: - RefCell) + 'bump>>>, + pub(crate) callback: RefCell) + 'bump>>>, } /// Virtual Components for custom user-defined components @@ -325,9 +321,9 @@ pub struct VComponent<'src> { // Function pointer to the FC that was used to generate this component pub user_fc: *const (), - pub(crate) caller: &'src dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, + pub(crate) caller: BumpBox<'src, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b> + 'src>, - pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>, + pub(crate) comparator: Option bool + 'src>>, pub(crate) drop_props: RefCell>>, @@ -342,7 +338,7 @@ pub struct VSuspended<'a> { pub dom_id: Cell>, #[allow(clippy::type_complexity)] - pub callback: RefCell) -> Element<'a>>>>, + pub callback: RefCell Element<'a> + 'a>>>, } /// This struct provides an ergonomic API to quickly build VNodes. @@ -491,7 +487,7 @@ impl<'a> NodeFactory<'a> { let raw_props = props as *mut P as *mut (); let user_fc = component as *const (); - let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(bump.alloc_with(|| { + let comparator: &mut dyn Fn(&VComponent) -> bool = bump.alloc_with(|| { move |other: &VComponent| { if user_fc == other.user_fc { // Safety @@ -504,8 +500,6 @@ impl<'a> NodeFactory<'a> { props.memoize(real_other) }; - log::debug!("comparing props..."); - // It's only okay to memoize if there are no children and the props can be memoized // Implementing memoize is unsafe and done automatically with the props trait props_memoized @@ -513,7 +507,8 @@ impl<'a> NodeFactory<'a> { false } } - })); + }); + let comparator = Some(unsafe { BumpBox::from_raw(comparator) }); let drop_props = { // create a closure to drop the props @@ -547,12 +542,14 @@ impl<'a> NodeFactory<'a> { let props: &'_ P = unsafe { &*(raw_props as *const P) }; let scp: &'a ScopeInner = unsafe { std::mem::transmute(scope) }; - let s: Scope<'a, P> = (Context { scope: scp }, props); + let s: Scope<'a, P> = (scp, props); let res: Element = component(s); unsafe { std::mem::transmute(res) } }); + let caller = unsafe { BumpBox::from_raw(caller) }; + VNode::Component(bump.alloc(VComponent { user_fc, comparator, diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index fb0ee8f69..f6c5ee018 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -142,6 +142,9 @@ pub(crate) struct Scheduler { // Garbage stored pub pending_garbage: FxHashSet, + // Every component that has futures that need to be polled + pub pending_futures: FxHashSet, + // In-flight futures pub async_tasks: FuturesUnordered, @@ -268,6 +271,7 @@ impl Scheduler { garbage_scopes: HashSet::new(), + pending_futures: Default::default(), dirty_scopes: Default::default(), saved_state: Some(saved_state), in_progress: false, diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index ec5f5ad33..f48163710 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,4 +1,5 @@ use crate::innerlude::*; + use fxhash::FxHashMap; use std::{ any::{Any, TypeId}, @@ -9,6 +10,36 @@ use std::{ 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 { +/// name: String +/// } +/// +/// fn example(cx: Context) -> VNode { +/// html! { +///
"Hello, {cx.name}"
+/// } +/// } +/// ``` +pub type Context<'a> = &'a ScopeInner; + /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -28,7 +59,7 @@ pub struct ScopeInner { // Nodes pub(crate) frames: ActiveFrame, - pub(crate) caller: *const dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, + pub(crate) caller: BumpBox<'static, dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>>, /* we care about: @@ -40,6 +71,9 @@ pub struct ScopeInner { pub(crate) borrowed_props: RefCell>>, pub(crate) suspended_nodes: RefCell>>, + pub(crate) tasks: RefCell>>>, + pub(crate) pending_effects: RefCell>>, + // State pub(crate) hooks: HookList, @@ -175,7 +209,7 @@ impl ScopeInner { // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom pub(crate) fn new( - caller: &dyn for<'b> Fn(&'b ScopeInner) -> Element<'b>, + caller: BumpBox Fn(&'b ScopeInner) -> Element<'b>>, our_arena_idx: ScopeId, parent_idx: Option, height: u32, @@ -186,8 +220,6 @@ impl ScopeInner { let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); - let caller = caller as *const _; - // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph let caller = unsafe { std::mem::transmute(caller) }; @@ -200,9 +232,11 @@ impl ScopeInner { 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(), @@ -297,14 +331,8 @@ impl ScopeInner { if let Some(suspended) = nodes.remove(&task_id) { let sus: &'a VSuspended<'static> = unsafe { &*suspended }; let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; - - let cx: SuspendedContext<'a> = SuspendedContext { - inner: Context { scope: self }, - }; - - let mut cb = sus.callback.borrow_mut().take().unwrap(); - - let new_node: Element<'a> = (cb)(cx); + let mut boxed = sus.callback.borrow_mut().take().unwrap(); + let new_node: Element<'a> = boxed(); } } @@ -372,4 +400,233 @@ impl ScopeInner { 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) + } + + /// Schedule an update for any component given its ScopeId. + /// + /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method. + /// + /// This method should be used when you want to schedule an update for a component + pub fn schedule_update_any(&self) -> Rc { + self.shared.schedule_any_immediate.clone() + } + + /// Get the [`ScopeId`] of a mounted component. + /// + /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. + pub fn bump(&self) -> &Bump { + let bump = &self.frames.wip_frame().bump; + bump + } + + /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. + /// + /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. + /// + /// ## Example + /// + /// ```ignore + /// fn Component(cx: Context<()>) -> VNode { + /// // Lazy assemble the VNode tree + /// let lazy_tree = html! {
"Hello World"
}; + /// + /// // Actually build the tree and allocate it + /// cx.render(lazy_tree) + /// } + ///``` + pub fn render<'src>( + &'src self, + lazy_nodes: Option>, + ) -> Option> { + let bump = &self.frames.wip_frame().bump; + let factory = NodeFactory { bump }; + lazy_nodes.map(|f| f.call(factory)) + } + + /// Push an effect to be ran after the component has been successfully mounted to the dom + /// Returns the effect's position in the stack + pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize { + // this is some tricker to get around not being able to actually call fnonces + let mut slot = Some(effect); + let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()()); + + // wrap it in a type that will actually drop the contents + let boxed_fut = unsafe { BumpBox::from_raw(fut) }; + + // erase the 'src lifetime for self-referential storage + let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; + + self.pending_effects.borrow_mut().push(self_ref_fut); + self.pending_effects.borrow().len() - 1 + } + + /// Pushes the future onto the poll queue to be polled + /// The future is forcibly dropped if the component is not ready by the next render + pub fn push_task<'src>(&'src self, fut: impl Future + 'src) -> usize { + // allocate the future + let fut: &mut dyn Future = self.bump().alloc(fut); + + // wrap it in a type that will actually drop the contents + let boxed_fut: BumpBox> = unsafe { BumpBox::from_raw(fut) }; + + // erase the 'src lifetime for self-referential storage + let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; + + self.tasks.borrow_mut().push(self_ref_fut); + self.tasks.borrow().len() - 1 + } + + /// This method enables the ability to expose state to children further down the VirtualDOM Tree. + /// + /// This is a "fundamental" operation and should only be called during initialization of a hook. + /// + /// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead. + /// + /// When the component is dropped, so is the context. Be aware of this behavior when consuming + /// the context via Rc/Weak. + /// + /// # Example + /// + /// ``` + /// struct SharedState(&'static str); + /// + /// static App: FC<()> = |(cx, props)|{ + /// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {}); + /// rsx!(cx, Child {}) + /// } + /// + /// static Child: FC<()> = |(cx, props)|{ + /// let state = cx.consume_state::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn provide_state(self, value: T) + where + T: 'static, + { + self.shared_contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(value)) + .map(|f| f.downcast::().ok()) + .flatten(); + } + + /// Try to retrieve a SharedState with type T from the any parent Scope. + pub fn consume_state(self) -> Option> { + 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()) + } + + /// Create a new subtree with this scope as the root of the subtree. + /// + /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route + /// the mutations to the correct window/portal/subtree. + /// + /// This method + /// + /// # Example + /// + /// ```rust + /// static App: FC<()> = |(cx, props)| { + /// todo!(); + /// rsx!(cx, div { "Subtree {id}"}) + /// }; + /// ``` + pub fn create_subtree(self) -> Option { + self.new_subtree() + } + + /// Get the subtree ID that this scope belongs to. + /// + /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route + /// the mutations to the correct window/portal/subtree. + /// + /// # Example + /// + /// ```rust + /// static App: FC<()> = |(cx, props)| { + /// let id = cx.get_current_subtree(); + /// rsx!(cx, div { "Subtree {id}"}) + /// }; + /// ``` + pub fn get_current_subtree(self) -> u32 { + self.subtree() + } + + /// Store a value between renders + /// + /// This is *the* foundational hook for all other hooks. + /// + /// - Initializer: closure used to create the initial hook state + /// - Runner: closure used to output a value every time the hook is used + /// - Cleanup: closure used to teardown the hook once the dom is cleaned up + /// + /// + /// # Example + /// + /// ```ignore + /// // use_ref is the simplest way of storing a value between renders + /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell { + /// use_hook( + /// || Rc::new(RefCell::new(initial_value())), + /// |state| state, + /// |_| {}, + /// ) + /// } + /// ``` + pub fn use_hook<'src, State, Output, Init, Run, Cleanup>( + &'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 + if self.hooks.at_end() { + self.hooks.push_hook( + initializer(self.hooks.len()), + Box::new(|raw| { + let s = raw.downcast::().unwrap(); + cleanup(s); + }), + ); + } + + runner(self.hooks.next::().expect(HOOK_ERR_MSG)) + } } + +const HOOK_ERR_MSG: &str = r###" +Unable to retrieve the hook that was initialized at this index. +Consult the `rules of hooks` to understand how to use hooks properly. + +You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. +Functions prefixed with "use" should never be called conditionally. +"###; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 7f2765c6f..6945520e4 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -64,7 +64,7 @@ pub struct VirtualDom { root_props: Rc, // we need to keep the allocation around, but we don't necessarily use it - _root_caller: Rc, + _root_caller: Box, } impl VirtualDom { @@ -143,28 +143,23 @@ impl VirtualDom { let props = root_props.clone(); - let root_caller: Rc Element> = Rc::new(move |scope: &ScopeInner| { - let props = props.downcast_ref::

().unwrap(); - let node = root((Context { scope }, props)); - // cast into the right lifetime - unsafe { std::mem::transmute(node) } - }); + let 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( - root_caller.as_ref(), - myidx, - None, - 0, - 0, - scheduler.pool.channel.clone(), - ) + ScopeInner::new(caller, myidx, None, 0, 0, scheduler.pool.channel.clone()) }); Self { - _root_caller: Rc::new(root_caller), + _root_caller: Box::new(root_caller), root_fc, base_scope, scheduler, @@ -223,7 +218,7 @@ impl VirtualDom { let root_caller: Box Element> = Box::new(move |scope: &ScopeInner| unsafe { let props: &'_ P = &*(props_ptr as *const P); - std::mem::transmute(root((Context { scope }, props))) + std::mem::transmute(root((scope, props))) }); root_scope.update_scope_dependencies(&root_caller); diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/core/tests/task.rs @@ -0,0 +1 @@ +fn main() {}