|
@@ -7,8 +7,8 @@ the DomTree trait is simply an abstraction over a lazy dom builder, much like th
|
|
|
|
|
|
This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
|
|
|
```ignore
|
|
|
-function Component(ctx: Context<()>) -> impl DomTree {
|
|
|
- html! {<div> "hello world" </div>}
|
|
|
+function Component(ctx: Context<()>) -> VNode {
|
|
|
+ ctx.view(html! {<div> "hello world" </div>})
|
|
|
}
|
|
|
```
|
|
|
It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
|
|
@@ -18,7 +18,7 @@ into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
|
|
|
```ignore
|
|
|
#[fc] // gets translated into a function.
|
|
|
static Component: FC = |ctx| {
|
|
|
- html! {<div> "hello world" </div>}
|
|
|
+ ctx.view(html! {<div> "hello world" </div>})
|
|
|
}
|
|
|
```
|
|
|
|
|
@@ -50,71 +50,153 @@ A Context
|
|
|
use crate::nodes::VNode;
|
|
|
use crate::prelude::*;
|
|
|
use bumpalo::Bump;
|
|
|
-use generational_arena::Arena;
|
|
|
-use std::future::Future;
|
|
|
+use generational_arena::{Arena, Index};
|
|
|
+use std::{
|
|
|
+ any::TypeId,
|
|
|
+ cell::{RefCell, UnsafeCell},
|
|
|
+ future::Future,
|
|
|
+ sync::atomic::AtomicUsize,
|
|
|
+};
|
|
|
|
|
|
/// An integrated virtual node system that progresses events and diffs UI trees.
|
|
|
/// Differences are converted into patches which a renderer can use to draw the UI.
|
|
|
-pub struct VirtualDom {
|
|
|
+pub struct VirtualDom<P: Properties> {
|
|
|
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
|
|
|
/// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
|
|
components: Arena<Scope>,
|
|
|
|
|
|
+ base_scope: Index,
|
|
|
+
|
|
|
/// Components generate lifecycle events
|
|
|
event_queue: Vec<LifecycleEvent>,
|
|
|
|
|
|
buffers: [Bump; 2],
|
|
|
+
|
|
|
+ selected_buf: u8,
|
|
|
+
|
|
|
+ root_props: P,
|
|
|
}
|
|
|
|
|
|
-impl VirtualDom {
|
|
|
+/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally.
|
|
|
+impl VirtualDom<()> {
|
|
|
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
|
|
|
///
|
|
|
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
|
|
|
/// The root component can access things like routing in its context.
|
|
|
pub fn new(root: FC<()>) -> Self {
|
|
|
- Self::new_with_props(root)
|
|
|
+ Self::new_with_props(root, ())
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
+/// Implement the VirtualDom for any Properties
|
|
|
+impl<P: Properties + 'static> VirtualDom<P> {
|
|
|
/// Start a new VirtualDom instance with a dependent props.
|
|
|
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
|
|
///
|
|
|
/// 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<P: Properties>(root: FC<P>) -> Self {
|
|
|
+ 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()];
|
|
|
+
|
|
|
+ // Arena allocate all the components
|
|
|
+ // This should make it *really* easy to store references in events and such
|
|
|
+ let mut components = Arena::new();
|
|
|
+
|
|
|
+ // Create a reference to the component in the arena
|
|
|
+ let base_scope = components.insert(Scope::new(root));
|
|
|
+
|
|
|
+ // Create an event queue with a mount for the base scope
|
|
|
+ let event_queue = vec![];
|
|
|
+
|
|
|
Self {
|
|
|
- components: Arena::new(),
|
|
|
- event_queue: vec![],
|
|
|
- buffers: [Bump::new(), Bump::new()],
|
|
|
+ components,
|
|
|
+ base_scope,
|
|
|
+ event_queue,
|
|
|
+ buffers,
|
|
|
+ root_props,
|
|
|
+ selected_buf: 0,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// Pop an event off the even queue and process it
|
|
|
- pub fn progress_event() {}
|
|
|
-}
|
|
|
+ pub fn progress(&mut self) -> Result<(), ()> {
|
|
|
+ let LifecycleEvent { index, event_type } = self.event_queue.pop().ok_or(())?;
|
|
|
|
|
|
-/// The internal lifecycle event system is managed by these
|
|
|
-/// All events need to be confused before swapping doms over
|
|
|
-pub enum LifecycleEvent {
|
|
|
- Add {},
|
|
|
-}
|
|
|
+ let scope = self.components.get(index).ok_or(())?;
|
|
|
+
|
|
|
+ match event_type {
|
|
|
+ // Component needs to be mounted to the virtual dom
|
|
|
+ LifecycleType::Mount {} => {
|
|
|
+ // todo! run the FC with the bump allocator
|
|
|
+ // Run it with its properties
|
|
|
+ }
|
|
|
+
|
|
|
+ // The parent for this component generated new props and the component needs update
|
|
|
+ LifecycleType::PropsChanged {} => {}
|
|
|
|
|
|
-/// Anything that takes a "bump" and returns VNodes is a "DomTree"
|
|
|
-/// This is used as a "trait alias" for function return types to look less hair
|
|
|
-pub trait DomTree {
|
|
|
- fn render(self, b: &Bump) -> VNode;
|
|
|
+ // Component was successfully mounted to the dom
|
|
|
+ LifecycleType::Mounted {} => {}
|
|
|
+
|
|
|
+ // Component was removed from the DOM
|
|
|
+ // Run any destructors and cleanup for the hooks and the dump the component
|
|
|
+ LifecycleType::Removed {} => {
|
|
|
+ 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 => {}
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Update the root props, causing a full event cycle
|
|
|
+ pub fn update_props(&mut self, new_props: P) {}
|
|
|
+
|
|
|
+ /// Run through every event in the event queue until the events are empty.
|
|
|
+ /// Function is asynchronous to allow for async components to finish their work.
|
|
|
+ pub async fn progess_completely() {}
|
|
|
+
|
|
|
+ /// Create a new context object for a given component and scope
|
|
|
+ fn new_context<T: Properties>(&self) -> Context<T> {
|
|
|
+ todo!()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Stop writing to the current buffer and start writing to the new one.
|
|
|
+ /// This should be done inbetween CallbackEvent handling, but not between lifecycle events.
|
|
|
+ pub fn swap_buffers(&mut self) {}
|
|
|
}
|
|
|
|
|
|
-/// Implement DomTree for the type returned by the html! macro.
|
|
|
-/// This lets the caller of the static function evaluate the builder closure with its own bump.
|
|
|
-/// It keeps components pretty and removes the need for the user to get too involved with allocation.
|
|
|
-impl<F> DomTree for F
|
|
|
-where
|
|
|
- F: FnOnce(&Bump) -> VNode,
|
|
|
-{
|
|
|
- fn render(self, b: &Bump) -> VNode {
|
|
|
- self(b)
|
|
|
+pub struct LifecycleEvent {
|
|
|
+ pub index: Index,
|
|
|
+ pub event_type: LifecycleType,
|
|
|
+}
|
|
|
+impl LifecycleEvent {
|
|
|
+ fn mount(index: Index) -> Self {
|
|
|
+ Self {
|
|
|
+ index,
|
|
|
+ event_type: LifecycleType::Mount,
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+/// The internal lifecycle event system is managed by these
|
|
|
+pub enum LifecycleType {
|
|
|
+ Mount,
|
|
|
+ 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>
|
|
@@ -152,37 +234,38 @@ impl Properties for () {
|
|
|
#[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<'a>(ctx: &'a Context<()>) -> VNode<'a> {
|
|
|
+ // todo: helper should be part of html! macro
|
|
|
+ todo!()
|
|
|
+ // ctx.view(|bump| html! {bump, <div> </div> })
|
|
|
+ }
|
|
|
+
|
|
|
+ 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();
|
|
|
|
|
|
- // // Make sure this function builds properly.
|
|
|
- // fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC<P, F>) -> VNode<'a> {
|
|
|
- // let p = P::new(); // new props
|
|
|
- // let c = Context { props: p }; // new context with props
|
|
|
- // let g = r(&c); // calling function with context
|
|
|
- // g.render(&b) // rendering closure with bump allocator
|
|
|
- // }
|
|
|
-
|
|
|
- // fn test_component(ctx: &Context<()>) -> impl DomTree {
|
|
|
- // // todo: helper should be part of html! macro
|
|
|
- // html! { <div> </div> }
|
|
|
- // }
|
|
|
-
|
|
|
- // fn test_component2(ctx: &Context<()>) -> impl DomTree {
|
|
|
- // __domtree_helper(move |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();
|
|
|
-
|
|
|
- // let g: FC<_, _> = test_component;
|
|
|
- // let nodes0 = test_static_fn(&b, &g);
|
|
|
- // // Happiness! The VNodes are now allocated onto the bump vdom
|
|
|
-
|
|
|
- // let g: FC<_, _> = test_component2;
|
|
|
- // let nodes1 = test_static_fn(&b, &g);
|
|
|
- // }
|
|
|
+ // 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
|
|
@@ -190,19 +273,30 @@ mod fc_test {
|
|
|
/// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
|
|
|
pub struct Scope {
|
|
|
hook_idx: i32,
|
|
|
- hooks: Vec<()>,
|
|
|
+ hooks: Vec<OLDHookState>,
|
|
|
+ props_type: TypeId,
|
|
|
}
|
|
|
|
|
|
impl Scope {
|
|
|
- fn new<T>() -> Self {
|
|
|
+ // create a new scope from a function
|
|
|
+ fn new<T: 'static>(f: FC<T>) -> Self {
|
|
|
+ // Capture the props type
|
|
|
+ let props_type = TypeId::of::<T>();
|
|
|
+
|
|
|
+ // Obscure the function
|
|
|
Self {
|
|
|
hook_idx: 0,
|
|
|
hooks: vec![],
|
|
|
+ props_type,
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /// 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() {}
|
|
|
}
|
|
|
|
|
|
-pub struct HookState {}
|
|
|
+pub struct OLDHookState {}
|
|
|
|
|
|
/// 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.
|
|
@@ -224,13 +318,16 @@ pub struct HookState {}
|
|
|
/// ```
|
|
|
// 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<'source, T> {
|
|
|
+pub struct Context<'src, T> {
|
|
|
/// Direct access to the properties used to create this component.
|
|
|
- pub props: &'source T,
|
|
|
+ pub props: T,
|
|
|
+ pub idx: AtomicUsize,
|
|
|
+ pub arena: &'src typed_arena::Arena<Hook>,
|
|
|
+ pub hooks: RefCell<Vec<*mut Hook>>,
|
|
|
+ pub _p: std::marker::PhantomData<&'src ()>,
|
|
|
}
|
|
|
|
|
|
impl<'a, T> Context<'a, T> {
|
|
|
- // 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")
|
|
@@ -271,4 +368,78 @@ impl<'a, T> Context<'a, T> {
|
|
|
) -> VNode<'a> {
|
|
|
todo!()
|
|
|
}
|
|
|
+
|
|
|
+ /// use_hook provides a way to store data between renders for functional components.
|
|
|
+ pub fn use_hook<'comp, InternalHookState: 'static, Output: 'static>(
|
|
|
+ &'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 for<'b> FnOnce(&'comp mut InternalHookState) -> &'comp 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
|
|
|
+ tear_down: impl FnOnce(InternalHookState),
|
|
|
+ ) -> &'comp 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 use 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
|
|
|
+ */
|
|
|
+ let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() };
|
|
|
+
|
|
|
+ let internal_state = borrowed_hook
|
|
|
+ .state
|
|
|
+ .downcast_mut::<InternalHookState>()
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ runner(internal_state)
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+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 {}
|