Feat: implememt nodes better

This commit is contained in:
Jonathan Kelley 2021-02-07 19:14:04 -05:00
parent c9d95dd1dc
commit edbb33b2ee
9 changed files with 317 additions and 387 deletions

View file

@ -1,4 +1,5 @@
# Project: Live-View 🤲 🍨
> Combine the server and client into a single file :)
# Project: Sanitization (TBD)
@ -8,17 +9,10 @@
# Project: Examples
> Get *all* the examples
- [ ] (Examples) Tide example with templating
- [ ] (Examples) Tide example with templating
- [ ] (Examples) Tide example with templating
- [ ] (Examples) Tide example with templating
# Project: State management
> Get some global state management installed with the hooks API
# Project: Hooks + Context + Subscriptions (TBD)
> Implement a light-weight string renderer with basic caching
- [ ] Implement context object
# Project: Concurrency (TBD)
> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted
@ -31,13 +25,26 @@
- [ ] (SSR) Implement stateful 3rd party string renderer
- [ ] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" in place of {format!("Text is {}",blah)}
# Project: Hooks + Context + Subscriptions (TBD)
> Implement the foundations for state management
- [x] Implement context object
- [ ] Implement use_state
- [ ] Implement use_ref
- [ ] Implement use_reducer
- [ ] Implement use_context
# Project: QOL
> Make it easier to write components
- [ ] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed)
- [ ] (Macro) Tweak component syntax to accept a new custom element
- [ ] (Macro) Allow components to specify their props as function args
# Project: Initial VDOM support (TBD)
> Get the initial VDom + Event System + Patching + Diffing + Component framework up and running
- [x] (Core) Migrate virtual node into new VNode type
- [ ] (Macro) Allow components to specify their props as function args
- [ ] (Core) Arena allocate VNodes
- [ ] (Core) Allow VNodes to borrow arena contents
- [ ] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures)
- [ ] (Macro) Tweak component syntax to accept a new custom element
- [ ] (Core) Introduce the VDOM and patch API for 3rd party renderers
- [x] (Core) Arena allocate VNodes
- [x] (Core) Allow VNodes to borrow arena contents
- [x] (Core) Introduce the VDOM and patch API for 3rd party renderers
- [ ] (Core) Implement lifecycle
- [ ] (Core) Implement an event system

View file

@ -6,7 +6,7 @@ This is the core crate for the Dioxus Virtual DOM. This README will focus on the
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
- React: hooks, concurrency, suspense
- Dodrio: bump allocation, double buffering
- Dodrio: bump allocation, double buffering, and source code for nodes + NodeBuilder
- Percy: html! macro architecture, platform-agnostic edits
- Yew: passion and inspiration ❤️
@ -17,7 +17,7 @@ We have big goals for Dioxus. The final implementation must:
- Be **fast**. Allocators are typically slow in WASM/Rust, so we should have a smart way of allocating.
- Be extremely memory efficient. Servers should handle tens of thousands of simultaneous VDoms with no problem.
- Be concurrent. Components should be able to pause rendering using a threading mechanism.
- Support "broadcasting". Edit lists should be separate from the Renderer implementation.
- Be "remote". Edit lists should be separate from the Renderer implementation.
- Support SSR. VNodes should render to a string that can be served via a web server.
- 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.
@ -25,8 +25,8 @@ We have big goals for Dioxus. The final implementation must:
## Optimizations
- Support a pluggable allocation strategy that makes VNode creation **very** fast
- Support lazy DomTrees (ie DomTrees that are not actually created when the view fn is ran)
- Support advanced diffing strategies
- Support lazy DomTrees (ie DomTrees that are not actually created when the html! macro is used)
- Support advanced diffing strategies (patience, Myers, etc)
## Design Quirks

View file

@ -138,7 +138,7 @@ fn test_use_state(ctx: Context<()>) -> VNode {
// Those vnodes are then tossed out and new ones are installed, meaning and old references (potentially bad)
// are removed and UB is prevented from /affecting/ the program
{
VNode::Element(VElement::new("button"))
VNode::text("blah")
}
}

View file

@ -0,0 +1,144 @@
use crate::nodes::VNode;
use crate::prelude::*;
use crate::scope::Hook;
use bumpalo::Bump;
use std::{
any::TypeId, cell::RefCell, future::Future, marker::PhantomData, sync::atomic::AtomicUsize,
};
/// 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>) -> VNode {
/// html! {
/// <div> "Hello, {ctx.props.name}" </div>
/// }
/// }
/// ```
// 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> {
/// Direct access to the properties used to create this component.
pub props: T,
pub idx: AtomicUsize,
// Borrowed from scope
pub(crate) arena: &'src typed_arena::Arena<Hook>,
pub(crate) hooks: &'src RefCell<Vec<*mut Hook>>,
// holder for the src lifetime
// todo @jon remove this
pub _p: std::marker::PhantomData<&'src ()>,
}
impl<'a, T> Context<'a, T> {
/// Access the children elements passed into the component
pub fn children(&self) -> Vec<VNode> {
todo!("Children API not yet implemented for component Context")
}
/// Access a parent context
pub fn parent_context<C>(&self) -> C {
todo!("Context API is not ready yet")
}
/// Create a subscription that schedules a future render for the reference component
pub fn subscribe(&self) -> impl FnOnce() -> () {
todo!("Subscription API is not ready yet");
|| {}
}
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
///
/// ```ignore
/// fn Component(ctx: Context<Props>) -> VNode {
/// // Lazy assemble the VNode tree
/// let lazy_tree = html! {<div>"Hello World"</div>};
///
/// // Actually build the tree and allocate it
/// ctx.view(lazy_tree)
/// }
///```
pub fn view(&self, v: impl FnOnce(&'a Bump) -> VNode<'a>) -> VNode<'a> {
todo!()
}
/// Create a suspended component from a future.
///
/// When the future completes, the component will be renderered
pub fn suspend(
&self,
fut: impl Future<Output = impl FnOnce(&'a Bump) -> VNode<'a>>,
) -> VNode<'a> {
todo!()
}
/// use_hook provides a way to store data between renders for functional components.
pub fn use_hook<'comp, InternalHookState: 'static, Output: 'comp>(
&'comp 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(&'comp 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 raw_hook = {
let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
// Mutate hook list if necessary
let mut hooks = self.hooks.borrow_mut();
// Initialize the hook by allocating it in the typed arena.
// We get a reference from the arena which is owned by the component scope
// This is valid because "Context" is only valid while the scope is borrowed
if idx >= hooks.len() {
let new_state = initializer();
let boxed_state: Box<dyn std::any::Any> = Box::new(new_state);
let hook = self.arena.alloc(Hook::new(boxed_state));
// Push the raw pointer instead of the &mut
// A "poor man's OwningRef"
hooks.push(hook);
}
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
*hooks.get(idx).unwrap()
};
/*
** UNSAFETY ALERT **
Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
into the arena. During the first call of the function, we need to add the mutable reference given to us by
the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
when the component itself is deallocated.
This is okay because:
- The lifetime of the component arena is tied to the lifetime of these raw hooks
- Usage of the raw hooks is tied behind the Vec refcell
- Output is static, meaning it can't take a reference to the data
- We don't expose the raw hook pointer outside of the scope of use_hook
- The reference is tied to context, meaning it can only be used while ctx is around to free it
*/
let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() };
let internal_state = borrowed_hook.0.downcast_mut::<InternalHookState>().unwrap();
// todo: set up an updater with the subscription API
let updater = ();
runner(internal_state, updater)
}
}

View file

@ -82,8 +82,9 @@ pub mod builder {
// types used internally that are important
pub(crate) mod inner {
pub use crate::component::{Component, Properties};
pub use crate::context::Context;
use crate::nodes;
pub use crate::scope::{Context, Hook, Scope};
pub use crate::scope::{Hook, Scope};
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;
@ -106,8 +107,8 @@ pub(crate) mod inner {
/// Essential when working with the html! macro
pub mod prelude {
pub use crate::component::{Component, Properties};
pub use crate::context::Context;
use crate::nodes;
pub use crate::scope::Context;
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;

View file

@ -1,7 +1,10 @@
//! Helpers for building virtual DOM nodes.
//! Helpers for building virtual DOM VNodes.
use crate::{
nodes::{Attribute, Listener, NodeKey, VNode},
prelude::VElement,
};
use crate::nodes::{Attribute, Listener, NodeKey, VNode};
type Node<'g> = VNode<'g>;
use bumpalo::Bump;
/// A virtual DOM element builder.
@ -14,7 +17,7 @@ pub struct ElementBuilder<'a, Listeners, Attributes, Children>
where
Listeners: 'a + AsRef<[Listener<'a>]>,
Attributes: 'a + AsRef<[Attribute<'a>]>,
Children: 'a + AsRef<[Node<'a>]>,
Children: 'a + AsRef<[VNode<'a>]>,
{
bump: &'a Bump,
key: NodeKey,
@ -30,7 +33,7 @@ impl<'a>
'a,
bumpalo::collections::Vec<'a, Listener<'a>>,
bumpalo::collections::Vec<'a, Attribute<'a>>,
bumpalo::collections::Vec<'a, Node<'a>>,
bumpalo::collections::Vec<'a, VNode<'a>>,
>
{
/// Create a new `ElementBuilder` for an element with the given tag name.
@ -77,7 +80,7 @@ impl<'a, Listeners, Attributes, Children> ElementBuilder<'a, Listeners, Attribut
where
Listeners: 'a + AsRef<[Listener<'a>]>,
Attributes: 'a + AsRef<[Attribute<'a>]>,
Children: 'a + AsRef<[Node<'a>]>,
Children: 'a + AsRef<[VNode<'a>]>,
{
/// Set the listeners for this element.
///
@ -187,7 +190,7 @@ where
#[inline]
pub fn children<C>(self, children: C) -> ElementBuilder<'a, Listeners, Attributes, C>
where
C: 'a + AsRef<[Node<'a>]>,
C: 'a + AsRef<[VNode<'a>]>,
{
ElementBuilder {
bump: self.bump,
@ -230,7 +233,7 @@ where
/// Set this element's key.
///
/// When diffing sets of siblings, if an old sibling and new sibling share a
/// key, then they will always reuse the same physical DOM node. This is
/// key, then they will always reuse the same physical DOM VNode. This is
/// important when using CSS animations, web components, third party JS, or
/// anything else that makes the diffing implementation observable.
///
@ -244,7 +247,7 @@ where
///
/// Keys must be unique among siblings.
///
/// All sibling nodes must be keyed, or they must all not be keyed. You may
/// All sibling VNodes must be keyed, or they must all not be keyed. You may
/// not mix keyed and unkeyed siblings.
///
/// # Example
@ -266,25 +269,25 @@ where
self
}
/// Create the virtual DOM node described by this builder.
/// Create the virtual DOM VNode described by this builder.
///
/// # Example
///
/// ```no_run
/// use dodrio::{builder::*, bumpalo::Bump, Node};
/// use dodrio::{builder::*, bumpalo::Bump, VNode};
///
/// let b = Bump::new();
///
/// // Start with a builder...
/// let builder: ElementBuilder<_, _, _> = div(&b);
///
/// // ...and finish it to create a virtual DOM node!
/// let my_div: Node = builder.finish();
/// // ...and finish it to create a virtual DOM VNode!
/// let my_div: VNode = builder.finish();
/// ```
#[inline]
pub fn finish(self) -> Node<'a> {
pub fn finish(self) -> VNode<'a> {
let children: &'a Children = self.bump.alloc(self.children);
let children: &'a [Node<'a>] = children.as_ref();
let children: &'a [VNode<'a>] = children.as_ref();
let listeners: &'a Listeners = self.bump.alloc(self.listeners);
let listeners: &'a [Listener<'a>] = listeners.as_ref();
@ -292,63 +295,62 @@ where
let attributes: &'a Attributes = self.bump.alloc(self.attributes);
let attributes: &'a [Attribute<'a>] = attributes.as_ref();
todo!()
// Node::element(
// self.bump,
// self.key,
// self.tag_name,
// listeners,
// attributes,
// children,
// self.namespace,
// )
VNode::element(
self.bump,
self.key,
self.tag_name,
listeners,
attributes,
children,
self.namespace,
)
}
}
// impl<'a, Attributes, Children>
// ElementBuilder<'a, bumpalo::collections::Vec<'a, Listener<'a>>, Attributes, Children>
// where
// Attributes: 'a + AsRef<[Attribute<'a>]>,
// Children: 'a + AsRef<[Node<'a>]>,
// {
// /// Add a new event listener to this element.
// ///
// /// The `event` string specifies which event will be listened for. The
// /// `callback` function is the function that will be invoked if the
// /// specified event occurs.
// ///
// /// # Example
// ///
// /// ```no_run
// /// use dodrio::{builder::*, bumpalo::Bump};
// ///
// /// let b = Bump::new();
// ///
// /// // A button that does something when clicked!
// /// let my_button = button(&b)
// /// .on("click", |root, vdom, event| {
// /// // ...
// /// })
// /// .finish();
// /// ```
// #[inline]
// pub fn on<F>(mut self, event: &'a str, callback: F) -> Self
// where
// F: 'static + Fn(&mut dyn RootRender, VdomWeak, web_sys::Event),
// {
// self.listeners.push(Listener {
// event,
// callback: self.bump.alloc(callback),
// });
// self
// }
// }
impl<'a, Attributes, Children>
ElementBuilder<'a, bumpalo::collections::Vec<'a, Listener<'a>>, Attributes, Children>
where
Attributes: 'a + AsRef<[Attribute<'a>]>,
Children: 'a + AsRef<[VNode<'a>]>,
{
/// Add a new event listener to this element.
///
/// The `event` string specifies which event will be listened for. The
/// `callback` function is the function that will be invoked if the
/// specified event occurs.
///
/// # Example
///
/// ```no_run
/// use dodrio::{builder::*, bumpalo::Bump};
///
/// let b = Bump::new();
///
/// // A button that does something when clicked!
/// let my_button = button(&b)
/// .on("click", |root, vdom, event| {
/// // ...
/// })
/// .finish();
/// ```
#[inline]
pub fn on<F>(mut self, event: &'a str, callback: F) -> Self
where
F: 'static + Fn(),
{
self.listeners.push(Listener {
event,
callback: self.bump.alloc(callback),
});
self
}
}
impl<'a, Listeners, Children>
ElementBuilder<'a, Listeners, bumpalo::collections::Vec<'a, Attribute<'a>>, Children>
where
Listeners: 'a + AsRef<[Listener<'a>]>,
Children: 'a + AsRef<[Node<'a>]>,
Children: 'a + AsRef<[VNode<'a>]>,
{
/// Add a new attribute to this element.
///
@ -404,7 +406,7 @@ where
}
impl<'a, Listeners, Attributes>
ElementBuilder<'a, Listeners, Attributes, bumpalo::collections::Vec<'a, Node<'a>>>
ElementBuilder<'a, Listeners, Attributes, bumpalo::collections::Vec<'a, VNode<'a>>>
where
Listeners: 'a + AsRef<[Listener<'a>]>,
Attributes: 'a + AsRef<[Attribute<'a>]>,
@ -425,7 +427,7 @@ where
/// .finish();
/// ```
#[inline]
pub fn child(mut self, child: Node<'a>) -> Self {
pub fn child(mut self, child: VNode<'a>) -> Self {
self.children.push(child);
self
}
@ -445,7 +447,7 @@ macro_rules! builder_constructors {
'a,
bumpalo::collections::Vec<'a, Listener<'a>>,
bumpalo::collections::Vec<'a, Attribute<'a>>,
bumpalo::collections::Vec<'a, Node<'a>>,
bumpalo::collections::Vec<'a, VNode<'a>>,
>
where
B: Into<&'a Bump>
@ -467,7 +469,7 @@ macro_rules! builder_constructors {
'a,
bumpalo::collections::Vec<'a, Listener<'a>>,
bumpalo::collections::Vec<'a, Attribute<'a>>,
bumpalo::collections::Vec<'a, Node<'a>>,
bumpalo::collections::Vec<'a, VNode<'a>>,
> {
let builder = ElementBuilder::new(bump, stringify!($name));
builder.namespace(Some($namespace))
@ -1018,9 +1020,9 @@ builder_constructors! {
image <> "http://www.w3.org/2000/svg";
}
/// Construct a text node.
/// Construct a text VNode.
///
/// This is `dodrio`'s virtual DOM equivalent of `document.createTextNode`.
/// This is `dodrio`'s virtual DOM equivalent of `document.createTextVNode`.
///
/// # Example
///
@ -1030,8 +1032,8 @@ builder_constructors! {
/// let my_text = text("hello, dodrio!");
/// ```
#[inline]
pub fn text<'a>(contents: &'a str) -> Node<'a> {
Node::text(contents)
pub fn text<'a>(contents: &'a str) -> VNode<'a> {
VNode::text(contents)
}
/// Construct an attribute for an element.

View file

@ -22,7 +22,7 @@ mod vnode {
pub enum VNode<'src> {
/// An element node (node type `ELEMENT_NODE`).
Element(VElement<'src>),
Element(&'src VElement<'src>),
/// A text node (node type `TEXT_NODE`).
///
@ -40,147 +40,54 @@ mod vnode {
Component(VComponent),
}
impl<'src> VNode<'src> {
/// Create a new virtual element node with a given tag.
impl<'a> VNode<'a> {
/// Low-level constructor for making a new `Node` of type element with given
/// parts.
///
/// These get patched into the DOM using `document.createElement`
///
/// ```ignore
/// let div = VNode::element("div");
/// ```
pub fn element(tag: &'static str) -> Self {
VNode::Element(VElement::new(tag))
/// This is primarily intended for JSX and templating proc-macros to compile
/// down into. If you are building nodes by-hand, prefer using the
/// `dodrio::builder::*` APIs.
#[inline]
pub fn element(
bump: &'a Bump,
key: NodeKey,
tag_name: &'a str,
listeners: &'a [Listener<'a>],
attributes: &'a [Attribute<'a>],
children: &'a [VNode<'a>],
namespace: Option<&'a str>,
) -> VNode<'a> {
let element = bump.alloc_with(|| VElement {
key,
tag_name,
listeners,
attributes,
children,
namespace,
});
VNode::Element(element)
}
/// Construct a new text node with the given text.
#[inline]
pub(crate) fn text(text: &'src str) -> VNode<'src> {
pub fn text(text: &'a str) -> VNode<'a> {
VNode::Text(VText { text })
}
// /// Create a new virtual text node with the given text.
// ///
// /// These get patched into the DOM using `document.createTextNode`
// ///
// /// ```ignore
// /// let div = VNode::text("div");
// /// ```
// pub fn text<S>(text: S) -> Self
// where
// S: Into<String>,
// {
// /*
// TODO
// This is an opportunity to be extremely efficient when allocating/creating strings
// To assemble a formatted string, we can, using the macro, borrow all the contents without allocating.
// String contents are therefore bump allocated automatically
// html!{
// <>"Hello {world}"</>
// }
// Should be
// ```
// let mut root = VNode::text(["Hello", world]);
// ```
// */
// VNode::Text(VText::new(text.into()))
// }
// /// Return a [`VElement`] reference, if this is an [`Element`] variant.
// ///
// /// [`VElement`]: struct.VElement.html
// /// [`Element`]: enum.VNode.html#variant.Element
// pub fn as_velement_ref(&self) -> Option<&VElement> {
// match self {
// VNode::Element(ref element_node) => Some(element_node),
// _ => None,
// }
// }
// /// Return a mutable [`VElement`] reference, if this is an [`Element`] variant.
// ///
// /// [`VElement`]: struct.VElement.html
// /// [`Element`]: enum.VNode.html#variant.Element
// pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
// match self {
// VNode::Element(ref mut element_node) => Some(element_node),
// _ => None,
// }
// }
// /// Return a [`VText`] reference, if this is an [`Text`] variant.
// ///
// /// [`VText`]: struct.VText.html
// /// [`Text`]: enum.VNode.html#variant.Text
// pub fn as_vtext_ref(&self) -> Option<&VText> {
// match self {
// VNode::Text(ref text_node) => Some(text_node),
// _ => None,
// }
// }
// /// Return a mutable [`VText`] reference, if this is an [`Text`] variant.
// ///
// /// [`VText`]: struct.VText.html
// /// [`Text`]: enum.VNode.html#variant.Text
// pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
// match self {
// VNode::Text(ref mut text_node) => Some(text_node),
// _ => None,
// }
// }
// /// Used by html-macro to insert space before text that is inside of a block that came after
// /// an open tag.
// ///
// /// html! { <div> {world}</div> }
// ///
// /// So that we end up with <div> world</div> when we're finished parsing.
// pub fn insert_space_before_text(&mut self) {
// match self {
// VNode::Text(text_node) => {
// text_node.text = " ".to_string() + &text_node.text;
// }
// _ => {}
// }
// }
// /// Used by html-macro to insert space after braced text if we know that the next block is
// /// another block or a closing tag.
// ///
// /// html! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
// /// html! { <div>{Hello} </div> } -> <div>Hello </div>
// ///
// /// So that we end up with <div>Hello world</div> when we're finished parsing.
// pub fn insert_space_after_text(&mut self) {
// match self {
// VNode::Text(text_node) => {
// text_node.text += " ";
// }
// _ => {}
// }
// }
#[inline]
pub(crate) fn key(&self) -> NodeKey {
match &self {
VNode::Text(_) => NodeKey::NONE,
VNode::Element(e) => e.key,
VNode::Suspended => {
todo!()
}
VNode::Component(_) => {
todo!()
}
}
}
}
// -----------------------------------------------
// Convert from DOM elements to the primary enum
// -----------------------------------------------
// impl From<VText> for VNode {
// fn from(other: VText) -> Self {
// VNode::Text(other)
// }
// }
// impl From<VElement> for VNode {
// fn from(other: VElement) -> Self {
// VNode::Element(other)
// }
// }
}
mod velement {
@ -188,11 +95,18 @@ mod velement {
use std::collections::HashMap;
pub struct VElement<'a> {
/// The HTML tag, such as "div"
pub tag: &'a str,
/// Elements have a tag name, zero or more attributes, and zero or more
pub key: NodeKey,
pub tag_name: &'a str,
pub listeners: &'a [Listener<'a>],
pub attributes: &'a [Attribute<'a>],
pub children: &'a [VNode<'a>],
pub namespace: Option<&'a str>,
// The HTML tag, such as "div"
// pub tag: &'a str,
// pub tag_name: &'a str,
// pub attributes: &'a [Attribute<'a>],
// todo: hook up listeners
// pub listeners: &'a [Listener<'a>],
// / HTML attributes such as id, class, style, etc

View file

@ -1,22 +1,23 @@
use crate::nodes::VNode;
use crate::prelude::*;
use any::Any;
use bumpalo::Bump;
use generational_arena::{Arena, Index};
use std::{
any::{self, TypeId},
cell::{RefCell, UnsafeCell},
future::Future,
marker::PhantomData,
sync::atomic::AtomicUsize,
any::TypeId, cell::RefCell, future::Future, marker::PhantomData, sync::atomic::AtomicUsize,
};
/// The Scope that wraps a functional component
/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components
/// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
/// 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 {
arena: typed_arena::Arena<Hook>,
// These hooks are actually references into the hook arena
// These two could be combined with "OwningRef" to remove unsafe usage
// TODO @Jon
hooks: RefCell<Vec<*mut Hook>>,
hook_arena: typed_arena::Arena<Hook>,
props_type: TypeId,
caller: *const i32,
}
@ -32,7 +33,7 @@ impl Scope {
let caller = f as *const i32;
Self {
arena,
hook_arena: arena,
hooks,
props_type,
caller,
@ -42,7 +43,7 @@ impl Scope {
pub fn create_context<T: Properties>(&mut self) -> Context<T> {
Context {
_p: PhantomData {},
arena: &self.arena,
arena: &self.hook_arena,
hooks: &self.hooks,
idx: 0.into(),
props: T::new(),
@ -74,152 +75,10 @@ impl Scope {
}
}
/// 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>) -> VNode {
/// html! {
/// <div> "Hello, {ctx.props.name}" </div>
/// }
/// }
/// ```
// 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> {
/// Direct access to the properties used to create this component.
pub props: T,
pub idx: AtomicUsize,
// Borrowed from scope
arena: &'src typed_arena::Arena<Hook>,
hooks: &'src RefCell<Vec<*mut Hook>>,
// holder for the src lifetime
// todo @jon remove this
pub _p: std::marker::PhantomData<&'src ()>,
}
impl<'a, T> Context<'a, T> {
/// Access the children elements passed into the component
pub fn children(&self) -> Vec<VNode> {
todo!("Children API not yet implemented for component Context")
}
/// Access a parent context
pub fn parent_context<C>(&self) -> C {
todo!("Context API is not ready yet")
}
/// Create a subscription that schedules a future render for the reference component
pub fn subscribe(&self) -> impl FnOnce() -> () {
todo!("Subscription API is not ready yet");
|| {}
}
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
///
/// ```ignore
/// fn Component(ctx: Context<Props>) -> VNode {
/// // Lazy assemble the VNode tree
/// let lazy_tree = html! {<div>"Hello World"</div>};
///
/// // Actually build the tree and allocate it
/// ctx.view(lazy_tree)
/// }
///```
pub fn view(&self, v: impl FnOnce(&'a Bump) -> VNode<'a>) -> VNode<'a> {
todo!()
}
/// Create a suspended component from a future.
///
/// When the future completes, the component will be renderered
pub fn suspend(
&self,
fut: impl Future<Output = impl FnOnce(&'a Bump) -> VNode<'a>>,
) -> VNode<'a> {
todo!()
}
/// use_hook provides a way to store data between renders for functional components.
pub fn use_hook<'comp, InternalHookState: 'static, Output: 'comp>(
&'comp 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(&'comp 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 raw_hook = {
let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
// Mutate hook list if necessary
let mut hooks = self.hooks.borrow_mut();
// Initialize the hook by allocating it in the typed arena.
// We get a reference from the arena which is owned by the component scope
// This is valid because "Context" is only valid while the scope is borrowed
if idx >= hooks.len() {
let new_state = initializer();
let boxed_state: Box<dyn std::any::Any> = Box::new(new_state);
let hook = self.arena.alloc(Hook::new(boxed_state));
// Push the raw pointer instead of the &mut
// A "poor man's OwningRef"
hooks.push(hook);
}
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
*hooks.get(idx).unwrap()
};
/*
** UNSAFETY ALERT **
Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
into the arena. During the first call of the function, we need to add the mutable reference given to us by
the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
when the component itself is deallocated.
This is okay because:
- The lifetime of the component arena is tied to the lifetime of these raw hooks
- Usage of the raw hooks is tied behind the Vec refcell
- Output is static, meaning it can't take a reference to the data
- We don't expose the raw hook pointer outside of the scope of use_hook
- The reference is tied to context, meaning it can only be used while ctx is around to free it
*/
let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() };
let internal_state = borrowed_hook
.state
.downcast_mut::<InternalHookState>()
.unwrap();
// todo: set up an updater with the subscription API
let updater = ();
runner(internal_state, updater)
}
}
pub struct Hook {
state: Box<dyn std::any::Any>,
}
pub struct Hook(pub Box<dyn std::any::Any>);
impl Hook {
fn new(state: Box<dyn std::any::Any>) -> Self {
Self { state }
pub fn new(state: Box<dyn std::any::Any>) -> Self {
Self(state)
}
}

View file

@ -1,6 +1,9 @@
// mod hooks;
// pub use hooks::use_context;
pub mod prelude {
use dioxus_core::prelude::Context;
pub fn use_state<T, G>(ctx: &mut Context<G>, init: impl Fn() -> T) -> (T, impl Fn(T)) {
pub fn use_state<T, G>(ctx: &Context<G>, init: impl Fn() -> T) -> (T, impl Fn(T)) {
let g = init();
(g, |_| {})
}