Feat: move out scope into its own file

This commit is contained in:
Jonathan Kelley 2021-02-07 17:38:17 -05:00
parent e939373616
commit c9d95dd1dc
15 changed files with 447 additions and 482 deletions

View file

@ -0,0 +1,23 @@
# This module includes all life-cycle related mechanics, including the virtual DOM, scopes, properties, and lifecycles.
---
The VirtualDom is designed as so:
VDOM contains:
- An arena of component scopes.
- A scope contains
- lifecycle data
- hook data
- Event queue
- An event
A VDOM is
- constructed from anything that implements "component"
A "Component" is anything (normally functions) that can be ran with a context to produce VNodes
- Must implement properties-builder trait which produces a properties builder
A Context
- Is a consumable struct
- Made of references to properties
- Holds a reference (lockable) to the underlying scope
- Is partially thread-safe

View file

@ -1,102 +1,9 @@
#![allow(unused, non_upper_case_globals)]
use bumpalo::Bump;
use dioxus_core::nodebuilder::*;
use dioxus_core::prelude::VNode;
use dioxus_core::prelude::*;
use dioxus_core::{nodebuilder::*, virtual_dom::Properties};
use once_cell::sync::{Lazy, OnceCell};
use std::{collections::HashMap, future::Future, marker::PhantomData};
fn main() {}
// struct VC<P, F = fn(Context<P>) -> VNode> {
// f: F,
// _a: std::marker::PhantomData<(P, F)>, // cell: OnceCell<T>,
// // init: Cell<Option<F>>
// }
// impl<P, F> VC<P, F> {
// const fn new(init: F) -> VC<P, F> {
// Self {
// _a: std::marker::PhantomData {},
// f: init,
// }
// }
// fn builder() -> P {
// // P::new()
// }
// }
// // Build a new functional component
// static SomeComp: VC<()> = VC::new(|ctx| {
// // This is a component, apparently
// // still not useful because we can't have bounds
// ctx.view(html! {
// <div>
// </div>
// })
// });
/*
*/
static BILL: Lazy<fn(Context<()>) -> String> = Lazy::new(|| {
//
|c| "BLAH".to_string()
});
// struct FUNC<F = fn() -> T> {}
struct SomeBuilder {}
// struct DummyRenderer {
// alloc: Bump,
// }
// impl DummyRenderer {
// // "Renders" a domtree by logging its children and outputs
// fn render() {}
// // Takes a domtree, an initial value, a new value, and produces the diff list
// fn produce_diffs() {}
// }
// struct Props<'a> {
// name: &'a str,
// }
// /// This component does "xyz things"
// /// This is sample documentation
// static Component: FC<Props> = |ctx| {
// // This block mimics that output of the html! macro
// DomTree::new(move |bump| {
// // parse into RSX structures
// // regurgetate as rust types
// // <div> "Child 1" "Child 2"</div>
// div(bump)
// .attr("class", "edit")
// .child(text("Child 1"))
// .child(text("Child 2"))
// .finish()
// })
// };
// /*
// source
// |> c1 -> VNode
// |> c2 -> VNode
// |> c3 -> VNode
// |> c4 -> VNode
// */

View file

@ -1,7 +1,7 @@
#![allow(unused, non_upper_case_globals, non_snake_case)]
use bumpalo::Bump;
use dioxus_core::nodebuilder::*;
use dioxus_core::prelude::*;
use dioxus_core::{nodebuilder::*, virtual_dom::Properties};
use std::{collections::HashMap, future::Future, marker::PhantomData};
fn main() {
@ -9,7 +9,7 @@ fn main() {
component,
Props {
blah: false,
text: "blah",
text: "blah".into(),
},
);
@ -45,11 +45,12 @@ fn main() {
// ~~~ Text shared between components via props can be done with lifetimes! ~~~
// Super duper efficient :)
struct Props<'src> {
struct Props {
blah: bool,
text: &'src str,
text: String,
// text: &'src str,
}
impl<'src> Properties for Props<'src> {
impl Properties for Props {
fn new() -> Self {
todo!()
}
@ -88,7 +89,7 @@ fn BuilderComp<'a>(ctx: &'a Context<'a, Props>) -> VNode<'a> {
div(bump)
.attr("class", "edit")
.child(text("Hello"))
.child(text(ctx.props.text))
.child(text(ctx.props.text.as_ref()))
.finish()
})
}
@ -99,23 +100,7 @@ fn EffcComp(ctx: &Context, name: &str) -> VNode {
// However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
// We can "view" them with Context for ultimate speed while inside components
// use "phase" style allocation;
/*
nodes...
text...
attrs...
<div> // node0
<div> </div> // node1
{// support some expression} // node 2
</div>
let node0;
let node1;
let node2 = evaluate{}.into();
let g= |bump| {1};
g(bump).into()
*/
// should we automatically view the output or leave it?
ctx.view(html! {
<div>
// your template goes here
@ -129,6 +114,8 @@ fn FullySuspended<'a>(ctx: &'a Context<Props>) -> VNode<'a> {
let i: i32 = 0;
// full suspended works great with just returning VNodes!
// Feel free to capture the html! macro directly
// Anything returned here is automatically viewed
let tex = match i {
1 => html! { <div> </div> },
2 => html! { <div> </div> },

View file

@ -1,8 +1,8 @@
use bumpalo::Bump;
use dioxus_core as dioxus;
use dioxus_core::{
prelude::{html, Context, VElement, VNode, FC},
virtual_dom::{Properties, Scope},
prelude::{html, Context, Properties, VElement, VNode, FC},
scope::Scope,
};
use std::{
any::Any,

View file

@ -5,17 +5,15 @@
//! render it again
//! consume the diffs and write that to a renderer
use dioxus_core::{
prelude::*,
virtual_dom::{Properties, Scope},
};
use dioxus_core::{prelude::*, scope::Scope};
fn main() {
let mut scope = Scope::new(Example);
let ctx = scope.create_context::<Props>();
fn main() -> Result<(), ()> {
let p1 = Props { name: "bob".into() };
let p2 = Props { name: "bob".into() };
let mut vdom = VirtualDom::new_with_props(Example, p1);
vdom.progress()?;
Ok(())
}
struct Props {

View file

@ -0,0 +1,58 @@
use crate::inner::*;
use crate::prelude::bumpalo::Bump;
/// The `Component` trait refers to any struct or funciton that can be used as a component
/// We automatically implement Component for FC<T>
pub trait Component {
type Props: Properties;
fn builder(&'static self) -> Self::Props;
}
// Auto implement component for a FC
// Calling the FC is the same as "rendering" it
impl<P: Properties> Component for FC<P> {
type Props = P;
fn builder(&self) -> Self::Props {
todo!()
}
}
/// The `Properties` trait defines any struct that can be constructed using a combination of default / optional fields.
/// Components take a "properties" object
pub trait Properties: 'static {
fn new() -> Self;
}
// Auto implement for no-prop components
impl Properties for () {
fn new() -> Self {
()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_static_fn<'a, P: Properties>(b: &'a Bump, r: FC<P>) -> VNode<'a> {
todo!()
}
fn test_component(ctx: Context<()>) -> VNode {
ctx.view(html! {<div> </div> })
}
fn test_component2(ctx: Context<()>) -> VNode {
ctx.view(|bump: &Bump| VNode::text("blah"))
}
#[test]
fn ensure_types_work() {
let bump = Bump::new();
// Happiness! The VNodes are now allocated onto the bump vdom
let _ = test_static_fn(&bump, test_component);
let _ = test_static_fn(&bump, test_component2);
}
}

View file

View file

@ -0,0 +1,35 @@
//! Debug virtual doms!
//! This renderer comes built in with dioxus core and shows how to implement a basic renderer.
//!
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
use crate::prelude::{Properties, VirtualDom};
pub struct DebugRenderer<'a, P: Properties> {
vdom: &'a mut VirtualDom<P>,
}
impl<'a, P: Properties> DebugRenderer<'a, P> {
pub fn new(vdom: &'a mut VirtualDom<P>) -> Self {
Self { vdom }
}
pub async fn run(&mut self) -> Result<(), ()> {
Ok(())
}
pub fn log_dom(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn ensure_creation() -> Result<(), ()> {
let mut dom = VirtualDom::new(|ctx| ctx.view(html! { <div>"hello world" </div> }));
dom.progress()?;
Ok(())
}
}

View file

@ -0,0 +1,26 @@
//! Virtual Events
//! This module provides a wrapping of platform-specific events with a list of events easier to work with.
//!
//! 3rd party renderers are responsible for forming this virtual events from events
//!
//! The goal here is to provide a consistent event interface across all renderer types
pub enum VirtualEvent {
ClipboardEvent,
CompositionEvent,
KeyboardEvent,
FocusEvent,
FormEvent,
GenericEvent,
MouseEvent,
PointerEvent,
SelectionEvent,
TouchEvent,
UIEvent,
WheelEvent,
MediaEvent,
ImageEvent,
AnimationEvent,
TransitionEvent,
OtherEvent,
}

View file

@ -65,8 +65,13 @@
//! - dioxus-liveview (SSR + StringRenderer)
//!
pub mod component;
pub mod context;
pub mod debug_renderer;
pub mod events;
pub mod nodebuilder;
pub mod nodes;
pub mod scope;
pub mod validation;
pub mod virtual_dom;
@ -74,33 +79,52 @@ pub mod builder {
pub use super::nodebuilder::*;
}
/// Re-export common types for ease of development use.
/// Essential when working with the html! macro
pub mod prelude {
// types used internally that are important
pub(crate) mod inner {
pub use crate::component::{Component, Properties};
use crate::nodes;
pub use crate::virtual_dom::{Context, VirtualDom};
pub use crate::scope::{Context, Hook, Scope};
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;
// pub use nodes::iterables::IterableNodes;
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
// pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
pub type VirtualNode<'a> = VNode<'a>;
// Re-export from the macro crate
// pub use dodrio_derive::html;
pub use bumpalo;
// pub use dioxus_html_macro::html;
// Re-export the FC macro
pub use crate as dioxus;
pub use crate::nodebuilder as builder;
pub use dioxus_core_macro::fc;
pub use dioxus_html_2::html;
}
/// Re-export common types for ease of development use.
/// Essential when working with the html! macro
pub mod prelude {
pub use crate::component::{Component, Properties};
use crate::nodes;
pub use crate::scope::Context;
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
pub type VirtualNode<'a> = VNode<'a>;
// expose our bumpalo type
pub use bumpalo;
// Re-export the FC macro
pub use crate as dioxus;
pub use crate::nodebuilder as builder;
pub use dioxus_core_macro::fc;
pub use dioxus_html_2::html;
pub use crate as dioxus;
pub use crate::nodebuilder as builder;
}

View file

@ -330,7 +330,7 @@ mod vtext {
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
mod vcomponent {
use crate::virtual_dom::Properties;
use crate::prelude::Properties;
use std::{any::TypeId, fmt, future::Future};
use super::VNode;

225
packages/core/src/scope.rs Normal file
View file

@ -0,0 +1,225 @@
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,
};
/// 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.
pub struct Scope {
arena: typed_arena::Arena<Hook>,
hooks: RefCell<Vec<*mut Hook>>,
props_type: TypeId,
caller: *const i32,
}
impl Scope {
// create a new scope from a function
pub fn new<T: 'static>(f: FC<T>) -> Self {
// Capture the props type
let props_type = TypeId::of::<T>();
let arena = typed_arena::Arena::new();
let hooks = RefCell::new(Vec::new());
let caller = f as *const i32;
Self {
arena,
hooks,
props_type,
caller,
}
}
pub fn create_context<T: Properties>(&mut self) -> Context<T> {
Context {
_p: PhantomData {},
arena: &self.arena,
hooks: &self.hooks,
idx: 0.into(),
props: T::new(),
}
}
/// 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
fn run<T: 'static>(&self, f: FC<T>) {}
fn call<T: Properties + 'static>(&mut self, val: T) {
if self.props_type == TypeId::of::<T>() {
/*
SAFETY ALERT
This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
This is safe because we check that the generic type matches before casting.
*/
let caller = unsafe { std::mem::transmute::<*const i32, FC<T>>(self.caller) };
let ctx = self.create_context::<T>();
// TODO: do something with these nodes
let nodes = caller(ctx);
} else {
panic!("Do not try to use `call` on Scopes with the wrong props type")
}
}
}
/// 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>,
}
impl Hook {
fn new(state: Box<dyn std::any::Any>) -> Self {
Self { state }
}
}

View file

@ -21,34 +21,9 @@ static Component: FC = |ctx| {
ctx.view(html! {<div> "hello world" </div>})
}
```
This module includes all life-cycle related mechanics, including the virtual dom, scopes, properties, and lifecycles.
---
The VirtualDom is designed as so:
VDOM contains:
- An arena of component scopes.
- A scope contains
- lifecycle data
- hook data
- Event queue
- An event
A VDOM is
- constructed from anything that implements "component"
A "Component" is anything (normally functions) that can be ran with a context to produce VNodes
- Must implement properties-builder trait which produces a properties builder
A Context
- Is a consumable struct
- Made of references to properties
- Holds a reference (lockable) to the underlying scope
- Is partially threadsafe
*/
use crate::inner::*;
use crate::nodes::VNode;
use crate::prelude::*;
use any::Any;
use bumpalo::Bump;
use generational_arena::{Arena, Index};
@ -67,15 +42,12 @@ pub struct VirtualDom<P: Properties> {
/// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
components: Arena<Scope>,
/// The index of the root component.
base_scope: Index,
/// Components generate lifecycle events
event_queue: Vec<LifecycleEvent>,
buffers: [Bump; 2],
selected_buf: u8,
root_props: P,
}
@ -98,12 +70,10 @@ impl<P: Properties + 'static> VirtualDom<P> {
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
/// to toss out the entire tree.
pub fn new_with_props(root: FC<P>, root_props: P) -> Self {
// 1. Create the buffers
// 2. Create the component arena
// 3. Create the base scope (can never be removed)
// 4. Create the lifecycle queue
// 5. Create the event queue
let buffers = [Bump::new(), Bump::new()];
// 1. Create the component arena
// 2. Create the base scope (can never be removed)
// 3. Create the lifecycle queue
// 4. Create the event queue
// Arena allocate all the components
// This should make it *really* easy to store references in events and such
@ -112,16 +82,17 @@ impl<P: Properties + 'static> VirtualDom<P> {
// Create a reference to the component in the arena
let base_scope = components.insert(Scope::new(root));
// Create a new mount event with no root container
let first_event = LifecycleEvent::mount(base_scope, None, 0);
// Create an event queue with a mount for the base scope
let event_queue = vec![];
let event_queue = vec![first_event];
Self {
components,
base_scope,
event_queue,
buffers,
root_props,
selected_buf: 0,
}
}
@ -133,9 +104,15 @@ impl<P: Properties + 'static> VirtualDom<P> {
match event_type {
// Component needs to be mounted to the virtual dom
LifecycleType::Mount {} => {
LifecycleType::Mount { to, under } => {
// todo! run the FC with the bump allocator
// Run it with its properties
if let Some(other) = to {
// mount to another component
let p = ();
} else {
// mount to the root
}
}
// The parent for this component generated new props and the component needs update
@ -150,10 +127,6 @@ impl<P: Properties + 'static> VirtualDom<P> {
let f = self.components.remove(index);
}
// Component was moved around in the DomTree
// Doesn't generate any event but interesting to keep track of
LifecycleType::Moved {} => {}
// Component was messaged via the internal subscription service
LifecycleType::Messaged => {}
}
@ -183,309 +156,18 @@ pub struct LifecycleEvent {
pub event_type: LifecycleType,
}
impl LifecycleEvent {
fn mount(index: Index) -> Self {
fn mount(which: Index, to: Option<Index>, under: usize) -> Self {
Self {
index,
event_type: LifecycleType::Mount,
index: which,
event_type: LifecycleType::Mount { to, under },
}
}
}
/// The internal lifecycle event system is managed by these
pub enum LifecycleType {
Mount,
Mount { to: Option<Index>, under: usize },
PropsChanged,
Mounted,
Removed,
Moved,
Messaged,
}
/// The `Component` trait refers to any struct or funciton that can be used as a component
/// We automatically implement Component for FC<T>
pub trait Component {
type Props: Properties;
fn builder(&'static self) -> Self::Props;
}
// Auto implement component for a FC
// Calling the FC is the same as "rendering" it
impl<P: Properties> Component for FC<P> {
type Props = P;
fn builder(&self) -> Self::Props {
todo!()
}
}
/// The `Properties` trait defines any struct that can be constructed using a combination of default / optional fields.
/// Components take a "properties" object
pub trait Properties {
fn new() -> Self;
}
// Auto implement for no-prop components
impl Properties for () {
fn new() -> Self {
()
}
}
// ============================================
// Compile Tests for FC/Component/Properties
// ============================================
#[cfg(test)]
mod fc_test {
use super::*;
use crate::prelude::*;
// Make sure this function builds properly.
fn test_static_fn<'a, P: Properties>(b: &'a Bump, r: FC<P>) -> VNode<'a> {
todo!()
// let p = P::new(); // new props
// let c = Context { props: &p }; // new context with props
// let g = r(&c); // calling function with context
// g
}
fn test_component(ctx: Context<()>) -> VNode {
// todo: helper should be part of html! macro
todo!()
// ctx.view(|bump| html! {bump, <div> </div> })
}
fn test_component2(ctx: Context<()>) -> VNode {
ctx.view(|bump: &Bump| VNode::text("blah"))
}
// fn test_component2<'a>(ctx: &'a Context<()>) -> VNode<'a> {
// ctx.view(|bump: &Bump| VNode::text("blah"))
// }
#[test]
fn ensure_types_work() {
// TODO: Get the whole casting thing to work properly.
// For whatever reason, FC is not auto-implemented, depsite it being a static type
let b = Bump::new();
// Happiness! The VNodes are now allocated onto the bump vdom
let nodes0 = test_static_fn(&b, test_component);
let nodes1 = test_static_fn(&b, test_component2);
}
}
/// 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.
pub struct Scope {
arena: typed_arena::Arena<Hook>,
hooks: RefCell<Vec<*mut Hook>>,
props_type: TypeId,
caller: *const (),
}
impl Scope {
// create a new scope from a function
pub fn new<T: 'static>(f: FC<T>) -> Self {
// Capture the props type
let props_type = TypeId::of::<T>();
let arena = typed_arena::Arena::new();
let hooks = RefCell::new(Vec::new());
let caller = f as *const ();
Self {
arena,
hooks,
props_type,
caller,
}
}
pub fn create_context<T: Properties>(&mut self) -> Context<T> {
Context {
_p: PhantomData {},
arena: &self.arena,
hooks: &self.hooks,
idx: 0.into(),
props: T::new(),
}
}
/// 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
fn run<T: 'static>(&self, f: FC<T>) {}
fn call<T: Properties + 'static>(&mut self, val: T) {
if self.props_type == TypeId::of::<T>() {
/*
SAFETY ALERT
This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
This is safe because we check that the generic type matches before casting.
*/
let caller = unsafe { std::mem::transmute::<*const (), FC<T>>(self.caller) };
let ctx = self.create_context::<T>();
let nodes = caller(ctx);
} else {
panic!("Do not try to use `call` on Scopes with the wrong props type")
}
}
}
/// 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>,
}
impl Hook {
fn new(state: Box<dyn std::any::Any>) -> Self {
Self { state }
}
}
/// A CallbackEvent wraps any event returned from the renderer's event system.
pub struct CallbackEvent {}
pub struct EventListener {}