diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs index aa42daef2..da4d7e961 100644 --- a/packages/core-macro/examples/prop_test.rs +++ b/packages/core-macro/examples/prop_test.rs @@ -2,8 +2,9 @@ fn main() {} pub mod dioxus { pub mod prelude { - pub trait Properties { + pub unsafe trait Properties { type Builder; + const CAN_BE_MEMOIZED: bool; fn builder() -> Self::Builder; } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index ef706043a..0301c68bd 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -679,8 +679,9 @@ Finally, call `.build()` to create the instance of `{name}`. } } - impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ + unsafe impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ type Builder = #builder_name #generics_with_empty; + const CAN_BE_MEMOIZED: bool = true; fn builder() -> Self::Builder { #name::builder() } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index bca37a866..6f197fa60 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -9,9 +9,9 @@ use generational_arena::Arena; use crate::innerlude::*; #[derive(Clone)] -pub struct ScopeArena(Rc>); +pub struct ScopeArena(pub Rc>); -struct ScopeArenaInner { +pub struct ScopeArenaInner { pub(crate) arena: UnsafeCell>, locks: HashMap, } @@ -61,6 +61,13 @@ impl ScopeArena { // todo!() } + pub fn try_remove(&mut self, id: ScopeIdx) -> Result { + let inner = unsafe { &mut *self.0.borrow().arena.get() }; + inner + .remove(id) + .ok_or_else(|| Error::FatalInternal("Scope not found")) + } + unsafe fn inner_unchecked<'s>() -> &'s mut Arena { todo!() } diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 22ad2106f..dc972c173 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -9,8 +9,9 @@ use crate::innerlude::FC; pub type ScopeIdx = generational_arena::Index; -pub trait Properties: PartialEq { +pub unsafe trait Properties: PartialEq { type Builder; + const CAN_BE_MEMOIZED: bool; fn builder() -> Self::Builder; } @@ -21,7 +22,8 @@ impl EmptyBuilder { } } -impl Properties for () { +unsafe impl Properties for () { + const CAN_BE_MEMOIZED: bool = true; type Builder = EmptyBuilder; fn builder() -> Self::Builder { diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 88c2b3331..80d0fd2ee 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -120,7 +120,7 @@ impl<'a> DiffMachine<'a> { When re-entering, we reuse the EditList in DiffState */ match (old, new) { - (VNode::Text(VText { text: old_text }), VNode::Text(VText { text: new_text })) => { + (VNode::Text(old_text), VNode::Text(new_text)) => { if old_text != new_text { self.change_list.commit_traversal(); self.change_list.set_text(new_text); @@ -157,30 +157,32 @@ impl<'a> DiffMachine<'a> { // self.change_list.commit_traversal(); if cold.user_fc == cnew.user_fc { // todo: create a stable addr - let caller = Rc::downgrade(&cnew.caller); - let id = cold.stable_addr.borrow().unwrap(); - *cnew.stable_addr.borrow_mut() = Some(id); - *cnew.ass_scope.borrow_mut() = *cold.ass_scope.borrow(); + // let caller = Rc::downgrade(&cnew.caller); + // let id = cold.stable_addr.borrow().unwrap(); + // *cnew.stable_addr.borrow_mut() = Some(id); + // *cnew.ass_scope.borrow_mut() = *cold.ass_scope.borrow(); - let scope = Rc::downgrade(&cold.ass_scope); - self.lifecycle_events - .push_back(LifeCycleEvent::PropsChanged { - caller, - root_id: id, - stable_scope_addr: scope, - }); + // let scope = Rc::downgrade(&cold.ass_scope); + todo!() + // self.lifecycle_events + // .push_back(LifeCycleEvent::PropsChanged { + // caller, + // root_id: id, + // stable_scope_addr: scope, + // }); } else { - let caller = Rc::downgrade(&cnew.caller); - let id = cold.stable_addr.borrow().unwrap(); - let old_scope = Rc::downgrade(&cold.ass_scope); - let new_scope = Rc::downgrade(&cnew.ass_scope); + // let caller = Rc::downgrade(&cnew.caller); + // let id = cold.stable_addr.borrow().unwrap(); + // let old_scope = Rc::downgrade(&cold.ass_scope); + // let new_scope = Rc::downgrade(&cnew.ass_scope); - self.lifecycle_events.push_back(LifeCycleEvent::Replace { - caller, - root_id: id, - old_scope, - new_scope, - }); + todo!() + // self.lifecycle_events.push_back(LifeCycleEvent::Replace { + // caller, + // root_id: id, + // old_scope, + // new_scope, + // }); } } @@ -217,7 +219,7 @@ impl<'a> DiffMachine<'a> { fn create(&mut self, node: &VNode<'a>) { debug_assert!(self.change_list.traversal_is_committed()); match node { - VNode::Text(VText { text }) => { + VNode::Text(text) => { self.change_list.create_text_node(text); } VNode::Element(&VElement { @@ -252,7 +254,7 @@ impl<'a> DiffMachine<'a> { // text content, and finally (3) append the text node to this // parent. if children.len() == 1 { - if let VNode::Text(VText { text }) = children[0] { + if let VNode::Text(text) = children[0] { self.change_list.set_text(text); return; } @@ -268,18 +270,20 @@ impl<'a> DiffMachine<'a> { todo: integrate re-entrace */ VNode::Component(component) => { - self.change_list - .create_text_node("placeholder for vcomponent"); + todo!() + // self.change_list + // .create_text_node("placeholder for vcomponent"); - let id = get_id(); - *component.stable_addr.as_ref().borrow_mut() = Some(id); - self.change_list.save_known_root(id); - let scope = Rc::downgrade(&component.ass_scope); - self.lifecycle_events.push_back(LifeCycleEvent::Mount { - caller: Rc::downgrade(&component.caller), - root_id: id, - stable_scope_addr: scope, - }); + // let id = get_id(); + // *component.stable_addr.as_ref().borrow_mut() = Some(id); + // self.change_list.save_known_root(id); + // let scope = Rc::downgrade(&component.ass_scope); + // todo!() + // self.lifecycle_events.push_back(LifeCycleEvent::Mount { + // caller: Rc::downgrade(&component.caller), + // root_id: id, + // stable_scope_addr: scope, + // }); } VNode::Suspended => { todo!("Creation of VNode::Suspended not yet supported") @@ -422,14 +426,11 @@ impl<'a> DiffMachine<'a> { if new.len() == 1 { match (old.first(), &new[0]) { - ( - Some(&VNode::Text(VText { text: old_text })), - &VNode::Text(VText { text: new_text }), - ) if old_text == new_text => { + (Some(&VNode::Text(old_text)), &VNode::Text(new_text)) if old_text == new_text => { // Don't take this fast path... } - (_, &VNode::Text(VText { text })) => { + (_, &VNode::Text(text)) => { self.change_list.commit_traversal(); self.change_list.set_text(text); // for o in old { @@ -979,11 +980,12 @@ impl<'a> DiffMachine<'a> { // self.change_list // .create_text_node("placeholder for vcomponent"); - let root_id = vcomp.stable_addr.as_ref().borrow().unwrap(); - self.lifecycle_events.push_back(LifeCycleEvent::Remove { - root_id, - stable_scope_addr: Rc::downgrade(&vcomp.ass_scope), - }) + todo!() + // let root_id = vcomp.stable_addr.as_ref().borrow().unwrap(); + // self.lifecycle_events.push_back(LifeCycleEvent::Remove { + // root_id, + // stable_scope_addr: Rc::downgrade(&vcomp.ass_scope), + // }) // let id = get_id(); // *component.stable_addr.as_ref().borrow_mut() = Some(id); // self.change_list.save_known_root(id); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 83ab31c98..53ea356b4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -86,6 +86,8 @@ pub mod virtual_dom; // Most fun logic starts here, manages the lifecycle and su pub mod builder { pub use super::nodebuilder::*; } +pub mod scope; +pub mod support; // types used internally that are important pub(crate) mod innerlude { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 388e8ae74..f6d02becc 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -27,7 +27,7 @@ pub enum VNode<'src> { Element(&'src VElement<'src>), /// A text node (node type `TEXT_NODE`). - Text(VText<'src>), + Text(&'src str), /// A fragment is a "virtual position" in the DOM /// Fragments may have children and keys @@ -46,11 +46,11 @@ pub enum VNode<'src> { impl<'a> Clone for VNode<'a> { fn clone(&self) -> Self { match self { - VNode::Element(el) => VNode::Element(el), - VNode::Text(origi) => VNode::Text(VText { text: origi.text }), - VNode::Fragment(frag) => VNode::Fragment(frag), + VNode::Element(element) => VNode::Element(element), + VNode::Text(text) => VNode::Text(text), + VNode::Fragment(fragment) => VNode::Fragment(fragment), + VNode::Component(component) => VNode::Component(component), VNode::Suspended => VNode::Suspended, - VNode::Component(c) => VNode::Component(c), } } } @@ -86,7 +86,7 @@ impl<'a> VNode<'a> { /// Construct a new text node with the given text. #[inline] pub fn text(text: &'a str) -> VNode<'a> { - VNode::Text(VText { text }) + VNode::Text(text) } pub fn text_args(bump: &'a Bump, args: Arguments) -> VNode<'a> { @@ -210,47 +210,31 @@ impl<'a> NodeKey<'a> { } } -#[derive(Debug, PartialEq)] -pub struct VText<'bump> { - pub text: &'bump str, -} - -impl<'b> Clone for VText<'b> { - fn clone(&self) -> Self { - Self { text: self.text } - } -} - -impl<'a> VText<'a> { - // / Create an new `VText` instance with the specified text. - pub fn new(text: &'a str) -> Self { - VText { text: text.into() } - } -} - // ============================== // Custom components // ============================== /// Virtual Components for custom user-defined components /// Only supports the functional syntax -pub type StableScopeAddres = RefCell>; -pub type VCompAssociatedScope = RefCell>; +pub type StableScopeAddres = Option; +pub type VCompAssociatedScope = Option; pub struct VComponent<'src> { pub key: NodeKey<'src>, - pub stable_addr: Rc, - pub ass_scope: Rc, + pub stable_addr: RefCell, + pub ass_scope: RefCell, // pub comparator: Rc bool + 'src>, pub caller: Rc VNode + 'src>, pub children: &'src [VNode<'src>], + pub comparator: Option<&'src dyn Fn(&VComponent) -> bool>, + // a pointer into the bump arena (given by the 'src lifetime) // raw_props: Box, - // raw_props: *const (), + raw_props: *const (), // a pointer to the raw fn typ pub user_fc: *const (), @@ -264,7 +248,8 @@ impl<'a> VComponent<'a> { // TODO: lift the requirement that props need to be static // we want them to borrow references... maybe force implementing a "to_static_unsafe" trait - pub fn new( + pub fn new( + bump: &'a Bump, component: FC

, // props: bumpalo::boxed::Box<'a, P>, props: P, @@ -273,25 +258,32 @@ impl<'a> VComponent<'a> { // pub fn new(component: FC

, props: P, key: Option<&'a str>) -> Self { // let bad_props = unsafe { transmogrify(props) }; let caller_ref = component as *const (); + let props = bump.alloc(props); - // let raw_props = props as *const P as *const (); + let raw_props = props as *const P as *const (); - // let props_comparator = move |other: &VComponent| { - // // Safety: - // // We are guaranteed that the props will be of the same type because - // // there is no way to create a VComponent other than this `new` method. - // // - // // Therefore, if the render functions are identical (by address), then so will be - // // props type paramter (because it is the same render function). Therefore, we can be - // // sure - // if caller_ref == other.user_fc { - // let g = other.raw_ctx.downcast_ref::

().unwrap(); - // // let real_other = unsafe { &*(other.raw_props as *const _ as *const P) }; - // &props == g - // } else { - // false - // } - // }; + let comparator: Option<&dyn Fn(&VComponent) -> bool> = { + if P::CAN_BE_MEMOIZED { + Some(bump.alloc(move |other: &VComponent| { + // Safety: + // We are guaranteed that the props will be of the same type because + // there is no way to create a VComponent other than this `new` method. + // + // Therefore, if the render functions are identical (by address), then so will be + // props type paramter (because it is the same render function). Therefore, we can be + // sure + if caller_ref == other.user_fc { + // let g = other.raw_ctx.downcast_ref::

().unwrap(); + let real_other = unsafe { &*(other.raw_props as *const _ as *const P) }; + &props == &real_other + } else { + false + } + })) + } else { + None + } + }; // let prref: &'a P = props.as_ref(); @@ -313,16 +305,18 @@ impl<'a> VComponent<'a> { None => NodeKey(None), }; + // raw_props: Box::new(props), + // comparator: Rc::new(props_comparator), Self { key, - ass_scope: Rc::new(RefCell::new(None)), + ass_scope: RefCell::new(None), user_fc: caller_ref, - // raw_props: Box::new(props), + comparator, + raw_props, _p: PhantomData, children: &[], caller, - // comparator: Rc::new(props_comparator), - stable_addr: Rc::new(RefCell::new(None)), + stable_addr: RefCell::new(None), } } } diff --git a/packages/core/src/patch.rs b/packages/core/src/patch.rs index 98d3959ba..6638ea1f2 100644 --- a/packages/core/src/patch.rs +++ b/packages/core/src/patch.rs @@ -146,6 +146,7 @@ pub struct EditMachine<'lock> { pub traversal: Traversal, next_temporary: u32, forcing_new_listeners: bool, + pub cur_height: u32, // // if the current node is a "known" node // // any actions that modify this node should update the mapping @@ -158,6 +159,7 @@ impl<'lock> EditMachine<'lock> { Self { // current_known: None, traversal: Traversal::new(), + cur_height: 0, next_temporary: 0, forcing_new_listeners: false, emitter: EditList::<'lock>::default(), diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs new file mode 100644 index 000000000..8018d7308 --- /dev/null +++ b/packages/core/src/scope.rs @@ -0,0 +1,488 @@ +use crate::{arena::ScopeArena, innerlude::*}; +use bumpalo::Bump; +use generational_arena::Arena; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + collections::{HashMap, HashSet, VecDeque}, + fmt::Debug, + future::Future, + ops::Deref, + pin::Pin, + rc::{Rc, Weak}, +}; + +/// Every component in Dioxus is represented by a `Scope`. +/// +/// Scopes contain the state for hooks, the component's props, and other lifecycle information. +/// +/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components. +/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. +pub struct Scope { + // The parent's scope ID + pub parent: Option, + + // IDs of children that this scope has created + // This enables us to drop the children and their children when this scope is destroyed + pub(crate) descendents: RefCell>, + + pub(crate) child_nodes: &'static [VNode<'static>], + + // A reference to the list of components. + // This lets us traverse the component list whenever we need to access our parent or children. + pub(crate) arena_link: ScopeArena, + + pub shared_contexts: RefCell>>, + + // Our own ID accessible from the component map + pub arena_idx: ScopeIdx, + + pub height: u32, + + pub event_channel: Rc, + + // pub event_queue: EventQueue, + pub caller: Weak>, + + pub hookidx: RefCell, + + // ========================== + // slightly unsafe stuff + // ========================== + // an internal, highly efficient storage of vnodes + pub frames: ActiveFrame, + + // These hooks are actually references into the hook arena + // These two could be combined with "OwningRef" to remove unsafe usage + // or we could dedicate a tiny bump arena just for them + // could also use ourborous + hooks: RefCell>, + + // Unsafety: + // - is self-refenrential and therefore needs to point into the bump + // Stores references into the listeners attached to the vnodes + // NEEDS TO BE PRIVATE + pub(crate) listeners: RefCell>, +} + +// We need to pin the hook so it doesn't move as we initialize the list of hooks +type Hook = Pin>; +type EventChannel = Rc; + +impl Scope { + // 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<'creator_node>( + caller: Weak>, + arena_idx: ScopeIdx, + parent: Option, + height: u32, + event_channel: EventChannel, + arena_link: ScopeArena, + child_nodes: &'creator_node [VNode<'creator_node>], + ) -> Self { + log::debug!( + "New scope created, height is {}, idx is {:?}", + height, + arena_idx + ); + + // The function to run this scope is actually located in the parent's bump arena. + // Every time the parent is updated, that function is invalidated via double-buffering wiping the old frame. + // If children try to run this invalid caller, it *will* result in UB. + // + // During the lifecycle progression process, this caller will need to be updated. Right now, + // until formal safety abstractions are implemented, we will just use unsafe to "detach" the caller + // lifetime from the bump arena, exposing ourselves to this potential for invalidation. Truthfully, + // this is a bit of a hack, but will remain this way until we've figured out a cleaner solution. + // + // Not the best solution, so TODO on removing this in favor of a dedicated resource abstraction. + let caller = unsafe { + std::mem::transmute::< + Weak>, + Weak>, + >(caller) + }; + + Self { + child_nodes: &[], + caller, + parent, + arena_idx, + height, + event_channel, + arena_link, + frames: ActiveFrame::new(), + hooks: Default::default(), + shared_contexts: Default::default(), + listeners: Default::default(), + hookidx: Default::default(), + descendents: Default::default(), + } + } + + pub fn update_caller<'creator_node>(&mut self, caller: Weak>) { + let broken_caller = unsafe { + std::mem::transmute::< + Weak>, + Weak>, + >(caller) + }; + + self.caller = broken_caller; + } + + /// Create a new context and run the component with references from the Virtual Dom + /// This function downcasts the function pointer based on the stored props_type + /// + /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) + pub fn run_scope<'sel>(&'sel mut self) -> Result<()> { + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. + self.frames.next().bump.reset(); + + // Remove all the outdated listeners + // + self.listeners + .try_borrow_mut() + .ok() + .ok_or(Error::FatalInternal("Borrowing listener failed"))? + .drain(..); + + *self.hookidx.borrow_mut() = 0; + + let caller = self + .caller + .upgrade() + .ok_or(Error::FatalInternal("Failed to get caller"))?; + + // Cast the caller ptr from static to one with our own reference + let c2: &OpaqueComponent<'static> = caller.as_ref(); + let c3: &OpaqueComponent<'_> = unsafe { std::mem::transmute(c2) }; + + let unsafe_head = unsafe { self.own_vnodes(c3) }; + + self.frames.cur_frame_mut().head_node = unsafe_head; + + Ok(()) + } + + // this is its own function so we can preciesly control how lifetimes flow + unsafe fn own_vnodes<'a>(&'a self, f: &OpaqueComponent<'a>) -> VNode<'static> { + let new_head: VNode<'a> = f(self); + let out: VNode<'static> = std::mem::transmute(new_head); + out + } + + // A safe wrapper around calling listeners + // calling listeners will invalidate the list of listeners + // The listener list will be completely drained because the next frame will write over previous listeners + pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { + let EventTrigger { + listener_id, event, .. + } = trigger; + // + unsafe { + // Convert the raw ptr into an actual object + // This operation is assumed to be safe + let listener_fn = self + .listeners + .try_borrow() + .ok() + .ok_or(Error::FatalInternal("Borrowing listener failed"))? + .get(listener_id as usize) + .ok_or(Error::FatalInternal("Event should exist if triggered"))? + .as_ref() + .ok_or(Error::FatalInternal("Raw event ptr is invalid"))?; + + // Run the callback with the user event + listener_fn(event); + } + Ok(()) + } + + pub fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { + self.frames.current_head_node() + } + + pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { + self.frames.prev_head_node() + } + + pub fn cur_frame(&self) -> &BumpFrame { + self.frames.cur_frame() + } +} + +/// Components in Dioxus use the "Context" object to interact with their lifecycle. +/// This lets components schedule updates, integrate hooks, and expose their context via the context api. +/// +/// Properties passed down from the parent component are also directly accessible via the exposed "props" field. +/// +/// ```ignore +/// #[derive(Properties)] +/// struct Props { +/// name: String +/// +/// } +/// +/// fn example(ctx: Context, props: &Props -> VNode { +/// html! { +///

"Hello, {ctx.ctx.name}"
+/// } +/// } +/// ``` +// todo: force lifetime of source into T as a valid lifetime too +// it's definitely possible, just needs some more messing around + +pub struct Context<'src, T> { + pub props: &'src T, + pub scope: &'src Scope, +} + +impl<'src, T> Copy for Context<'src, T> {} +impl<'src, T> Clone for Context<'src, T> { + fn clone(&self) -> Self { + Self { + props: self.props, + scope: self.scope, + } + } +} + +impl<'a, T> Deref for Context<'a, T> { + type Target = &'a T; + + fn deref(&self) -> &Self::Target { + &self.props + } +} + +impl<'src, T> Scoped<'src> for Context<'src, T> { + fn get_scope(&self) -> &'src Scope { + self.scope + } +} + +pub trait Scoped<'src>: Sized { + fn get_scope(&self) -> &'src Scope; + + /// Access the children elements passed into the component + fn children(&self) -> &'src [VNode<'src>] { + // We're re-casting the nodes back out + // They don't really have a static lifetime + unsafe { + let scope = self.get_scope(); + let nodes: &'src [VNode<'static>] = scope.child_nodes; + + // cast the lifetime back correctly + std::mem::transmute(nodes) + } + } + + /// Create a subscription that schedules a future render for the reference component + fn schedule_update(&self) -> Rc { + self.get_scope().event_channel.clone() + } + + /// 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(ctx: Context<()>) -> VNode { + /// // Lazy assemble the VNode tree + /// let lazy_tree = html! {
"Hello World"
}; + /// + /// // Actually build the tree and allocate it + /// ctx.render(lazy_tree) + /// } + ///``` + fn render<'a, F: for<'b> FnOnce(&'b NodeCtx<'src>) -> VNode<'src> + 'src + 'a>( + self, + lazy_nodes: LazyNodes<'src, F>, + ) -> VNode<'src> { + lazy_nodes.into_vnode(&NodeCtx { + scope_ref: self.get_scope(), + listener_id: 0.into(), + }) + } + + // impl<'scope> Context<'scope> { + /// Store a value between renders + /// + /// - 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 + /// + /// ```ignore + /// // use_ref is the simplest way of storing a value between renders + /// pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> { + /// use_hook( + /// || Rc::new(RefCell::new(initial_value())), + /// |state| state.clone(), + /// |_| {}, + /// ) + /// } + /// ``` + fn use_hook( + &self, + + // The closure that builds the hook state + initializer: impl FnOnce() -> InternalHookState, + + // The closure that takes the hookstate and returns some value + runner: impl FnOnce(&'src mut InternalHookState) -> Output, + + // The closure that cleans up whatever mess is left when the component gets torn down + // TODO: add this to the "clean up" group for when the component is dropped + _cleanup: impl FnOnce(InternalHookState), + ) -> Output { + let scope = self.get_scope(); + + let idx = *scope.hookidx.borrow(); + + // Grab out the hook list + let mut hooks = scope.hooks.borrow_mut(); + + // If the idx is the same as the hook length, then we need to add the current hook + if idx >= hooks.len() { + let new_state = initializer(); + hooks.push(Box::pin(new_state)); + } + + *scope.hookidx.borrow_mut() += 1; + + let stable_ref = hooks + .get_mut(idx) + .expect("Should not fail, idx is validated") + .as_mut(); + + let pinned_state = unsafe { Pin::get_unchecked_mut(stable_ref) }; + + let internal_state = pinned_state.downcast_mut::().expect( + r###" +Unable to retrive the hook that was initialized in 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. +Any function prefixed with "use" should not be called conditionally. + "###, + ); + + // We extend the lifetime of the internal state + runner(unsafe { &mut *(internal_state as *mut _) }) + } + + /// This hook enables the ability to expose state to children further down the VirtualDOM Tree. + /// + /// This is a hook, so it may not be called conditionally! + /// + /// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`) + /// so don't put it in a conditional. + /// + /// When the component is dropped, so is the context. Be aware of this behavior when consuming + /// the context via Rc/Weak. + /// + /// + /// + fn use_create_context(&self, init: impl Fn() -> T) { + let scope = self.get_scope(); + let mut ctxs = scope.shared_contexts.borrow_mut(); + let ty = TypeId::of::(); + + let is_initialized = self.use_hook( + || false, + |s| { + let i = s.clone(); + *s = true; + i + }, + |_| {}, + ); + + match (is_initialized, ctxs.contains_key(&ty)) { + // Do nothing, already initialized and already exists + (true, true) => {} + + // Needs to be initialized + (false, false) => { + log::debug!("Initializing context..."); + ctxs.insert(ty, Rc::new(init())); + } + + _ => debug_assert!(false, "Cannot initialize two contexts of the same type"), + } + } + + /// There are hooks going on here! + fn use_context(&self) -> &'src Rc { + self.try_use_context().unwrap() + } + + /// Uses a context, storing the cached value around + fn try_use_context(&self) -> Result<&'src Rc> { + struct UseContextHook { + par: Option>, + we: Option>, + } + + self.use_hook( + move || UseContextHook { + par: None as Option>, + we: None as Option>, + }, + move |hook| { + let scope = self.get_scope(); + let mut scope = Some(scope); + + if let Some(we) = &hook.we { + if let Some(re) = we.upgrade() { + hook.par = Some(re); + return Ok(hook.par.as_ref().unwrap()); + } + } + + let ty = TypeId::of::(); + while let Some(inner) = scope { + log::debug!("Searching {:#?} for valid shared_context", inner.arena_idx); + let shared_contexts = inner.shared_contexts.borrow(); + + if let Some(shared_ctx) = shared_contexts.get(&ty) { + log::debug!("found matching ctx"); + let rc = shared_ctx + .clone() + .downcast::() + .expect("Should not fail, already validated the type from the hashmap"); + + hook.we = Some(Rc::downgrade(&rc)); + hook.par = Some(rc); + return Ok(hook.par.as_ref().unwrap()); + } else { + match inner.parent { + Some(parent_id) => { + let parent = inner + .arena_link + .try_get(parent_id) + .map_err(|_| Error::FatalInternal("Failed to find parent"))?; + + scope = Some(parent); + } + None => return Err(Error::MissingSharedContext), + } + } + } + + Err(Error::MissingSharedContext) + }, + |_| {}, + ) + } +} diff --git a/packages/core/src/support.rs b/packages/core/src/support.rs new file mode 100644 index 000000000..2cbdd57b2 --- /dev/null +++ b/packages/core/src/support.rs @@ -0,0 +1,166 @@ +pub use crate::scope::*; +use crate::{arena::ScopeArena, innerlude::*}; +use bumpalo::Bump; +use generational_arena::Arena; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + collections::{HashMap, HashSet, VecDeque}, + fmt::Debug, + future::Future, + ops::Deref, + pin::Pin, + rc::{Rc, Weak}, +}; +// We actually allocate the properties for components in their parent's properties +// We then expose a handle to use those props for render in the form of "OpaqueComponent" +pub(crate) type OpaqueComponent<'e> = dyn Fn(&'e Scope) -> VNode<'e> + 'e; +// pub(crate) type OpaqueComponent<'e> = dyn for<'b> Fn(&'b Scope) -> VNode<'b> + 'e; + +#[derive(PartialEq, Debug, Clone, Default)] +pub(crate) struct EventQueue(pub Rc>>); + +impl EventQueue { + pub fn new_channel(&self, height: u32, idx: ScopeIdx) -> Rc { + let inner = self.clone(); + let marker = HeightMarker { height, idx }; + Rc::new(move || inner.0.as_ref().borrow_mut().push(marker)) + } +} + +/// A helper type that lets scopes be ordered by their height +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct HeightMarker { + pub idx: ScopeIdx, + pub height: u32, +} + +impl Ord for HeightMarker { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.height.cmp(&other.height) + } +} + +impl PartialOrd for HeightMarker { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// NodeCtx is used to build VNodes in the component's memory space. +// This struct adds metadata to the final VNode about listeners, attributes, and children +#[derive(Clone)] +pub struct NodeCtx<'a> { + pub scope_ref: &'a Scope, + pub listener_id: RefCell, +} + +impl<'a> NodeCtx<'a> { + pub fn bump(&self) -> &'a Bump { + &self.scope_ref.cur_frame().bump + } +} + +impl Debug for NodeCtx<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +#[derive(Debug, PartialEq, Hash)] +pub struct ContextId { + // Which component is the scope in + original: ScopeIdx, + + // What's the height of the scope + height: u32, + + // Which scope is it (in order) + id: u32, +} + +pub struct ActiveFrame { + // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken + pub generation: RefCell, + + // The double-buffering situation that we will use + pub frames: [BumpFrame; 2], +} + +pub struct BumpFrame { + pub bump: Bump, + pub head_node: VNode<'static>, +} + +impl ActiveFrame { + pub fn new() -> Self { + Self::from_frames( + BumpFrame { + bump: Bump::new(), + head_node: VNode::text(""), + }, + BumpFrame { + bump: Bump::new(), + head_node: VNode::text(""), + }, + ) + } + + pub(crate) fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { + Self { + generation: 0.into(), + frames: [a, b], + } + } + + pub(crate) fn cur_frame(&self) -> &BumpFrame { + match *self.generation.borrow() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + } + } + pub(crate) fn cur_frame_mut(&mut self) -> &mut BumpFrame { + match *self.generation.borrow() & 1 == 0 { + true => &mut self.frames[0], + false => &mut self.frames[1], + } + } + + pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { + let raw_node = match *self.generation.borrow() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + + // Give out our self-referential item with our own borrowed lifetime + unsafe { + let unsafe_head = &raw_node.head_node; + let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); + safe_node + } + } + + pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { + let raw_node = match *self.generation.borrow() & 1 != 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + + // Give out our self-referential item with our own borrowed lifetime + unsafe { + let unsafe_head = &raw_node.head_node; + let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); + safe_node + } + } + + pub(crate) fn next(&mut self) -> &mut BumpFrame { + *self.generation.borrow_mut() += 1; + + if *self.generation.borrow() % 2 == 0 { + &mut self.frames[0] + } else { + &mut self.frames[1] + } + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 34adc4335..20d590021 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -191,10 +191,6 @@ impl VirtualDom { // Ok(diff_machine.consume()) } - - pub fn base_scope(&self) -> &Scope { - todo!() - } } // ====================================== diff --git a/packages/recoil/src/lib.rs b/packages/recoil/src/lib.rs index e73428600..7fc684a14 100644 --- a/packages/recoil/src/lib.rs +++ b/packages/recoil/src/lib.rs @@ -32,7 +32,7 @@ mod traits { // Atoms, selectors, and their family variants are readable pub trait Readable: Sized + Copy { fn use_read<'a, P: 'static>(self, ctx: Context<'a, P>) -> &'a T { - hooks::use_read(ctx, self) + hooks::use_read(&ctx, self) } // This returns a future of the value @@ -95,9 +95,9 @@ mod atoms { const EXAMPLE_ATOM: Atom = |_| 10; // ensure that atoms are both read and write - let _ = use_read(ctx, &EXAMPLE_ATOM); - let _ = use_read_write(ctx, &EXAMPLE_ATOM); - let _ = use_write(ctx, &EXAMPLE_ATOM); + let _ = use_read(&ctx, &EXAMPLE_ATOM); + let _ = use_read_write(&ctx, &EXAMPLE_ATOM); + let _ = use_write(&ctx, &EXAMPLE_ATOM); } } } @@ -161,7 +161,7 @@ mod atomfamily { fn test(ctx: Context<()>) { let title = Titles.select(&10).use_read(ctx); - let t2 = use_read(ctx, &Titles.select(&10)); + let t2 = use_read(&ctx, &Titles.select(&10)); } } } @@ -368,7 +368,7 @@ mod root { mod hooks { use super::*; - use dioxus_core::{hooks::use_ref, prelude::Context}; + use dioxus_core::{hooks::use_ref, prelude::Context, scope::Scoped}; pub fn use_init_recoil_root

(ctx: Context

, cfg: impl Fn(())) { ctx.use_create_context(move || RefCell::new(RecoilRoot::new())) @@ -381,12 +381,12 @@ mod hooks { /// /// You can use this method to create controllers that perform much more complex actions than set/get /// However, be aware that "getting" values through this hook will not subscribe the component to any updates. - pub fn use_recoil_api<'a, P>(ctx: Context<'a, P>) -> &Rc { + pub fn use_recoil_api<'a>(ctx: &impl Scoped<'a>) -> &'a Rc { ctx.use_context::() } - pub fn use_write<'a, T: AtomValue, P>( - ctx: Context<'a, P>, + pub fn use_write<'a, T: AtomValue>( + ctx: &impl Scoped<'a>, // todo: this shouldn't need to be static writable: impl Writable, ) -> &'a Rc { @@ -412,10 +412,10 @@ mod hooks { /// Read the atom and get the Rc directly to the Atom's slot /// This is useful if you need the memoized Atom value. However, Rc is not as easy to /// work with as - pub fn use_read_raw<'a, T: AtomValue, P: 'static>( - ctx: Context<'a, P>, + pub fn use_read_raw<'a, T: AtomValue>( + ctx: &impl Scoped<'a>, readable: impl Readable, - ) -> &Rc { + ) -> &'a Rc { struct ReadHook { value: Rc, consumer_id: u32, @@ -449,10 +449,7 @@ mod hooks { } /// - pub fn use_read<'a, T: AtomValue, P: 'static>( - ctx: Context<'a, P>, - readable: impl Readable, - ) -> &'a T { + pub fn use_read<'a, T: AtomValue>(ctx: &impl Scoped<'a>, readable: impl Readable) -> &'a T { use_read_raw(ctx, readable).as_ref() } @@ -471,8 +468,8 @@ mod hooks { /// // equivalent to: /// let (title, set_title) = (use_read(ctx, &Title), use_write(ctx, &Title)); /// ``` - pub fn use_read_write<'a, T: AtomValue + 'static, P: 'static>( - ctx: Context<'a, P>, + pub fn use_read_write<'a, T: AtomValue + 'static>( + ctx: &impl Scoped<'a>, writable: impl Writable, ) -> (&'a T, &'a Rc) { (use_read(ctx, writable), use_write(ctx, writable))