Merge pull request #1 from jkelleyrtp/jk/dyn_scope
Feat: move to dyn Scope for arena
This commit is contained in:
6 files changed
+301
-347
No files matched your search
@@ -1,34 +1,19 @@
|
||||
//! An example that shows how to:
|
||||
//! create a scope,
|
||||
//! render a component,
|
||||
//! change some data
|
||||
//! render it again
|
||||
//! consume the diffs and write that to a renderer
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
|
||||
fn main() -> Result<(), ()> {
|
||||
let p1 = Props { name: "bob".into() };
|
||||
|
||||
let _vdom = VirtualDom::new_with_props(Example, p1);
|
||||
// vdom.progress()?;
|
||||
let mut vdom = VirtualDom::new_with_props(Example, p1);
|
||||
vdom.update_props(|p: &mut Props| {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Props {
|
||||
name: String,
|
||||
}
|
||||
|
||||
// impl Properties for Props {
|
||||
// fn call(&self, ptr: *const ()) {}
|
||||
|
||||
// // fn new() -> Self {
|
||||
// // todo!()
|
||||
// // }
|
||||
// }
|
||||
|
||||
static Example: FC<Props> = |ctx, _props| {
|
||||
ctx.render(html! {
|
||||
<div>
|
||||
|
||||
+58
-11
@@ -32,10 +32,14 @@
|
||||
//!
|
||||
//! More info on how to improve this diffing algorithm:
|
||||
//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/
|
||||
use crate::innerlude::*;
|
||||
use crate::{
|
||||
innerlude::*,
|
||||
scope::{create_scoped, Scoped},
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use std::cmp::Ordering;
|
||||
use generational_arena::Arena;
|
||||
use std::{cell::RefCell, cmp::Ordering, collections::VecDeque, rc::Rc};
|
||||
|
||||
/// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while
|
||||
/// diffing trees of components. This means we can "re-enter" a subtree of a component by queuing a "NeedToDiff" event.
|
||||
@@ -49,20 +53,23 @@ use std::cmp::Ordering;
|
||||
/// The order of these re-entrances is stored in the DiffState itself. The DiffState comes pre-loaded with a set of components
|
||||
/// that were modified by the eventtrigger. This prevents doubly evaluating components if they wereboth updated via
|
||||
/// subscriptions and props changes.
|
||||
pub struct DiffMachine<'a> {
|
||||
pub struct DiffMachine<'a, 'b> {
|
||||
pub change_list: EditMachine<'a>,
|
||||
immediate_queue: Vec<ScopeIdx>,
|
||||
diffed: FxHashSet<ScopeIdx>,
|
||||
need_to_diff: FxHashSet<ScopeIdx>,
|
||||
|
||||
pub vdom: &'b VirtualDom,
|
||||
pub cur_idx: ScopeIdx,
|
||||
pub diffed: FxHashSet<ScopeIdx>,
|
||||
pub need_to_diff: FxHashSet<ScopeIdx>,
|
||||
}
|
||||
|
||||
impl<'a> DiffMachine<'a> {
|
||||
pub fn new(bump: &'a Bump) -> Self {
|
||||
impl<'a, 'b> DiffMachine<'a, 'b> {
|
||||
pub fn new(vdom: &'b VirtualDom, bump: &'a Bump, idx: ScopeIdx) -> Self {
|
||||
Self {
|
||||
cur_idx: idx,
|
||||
change_list: EditMachine::new(bump),
|
||||
immediate_queue: Vec::new(),
|
||||
diffed: FxHashSet::default(),
|
||||
need_to_diff: FxHashSet::default(),
|
||||
vdom,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +78,7 @@ impl<'a> DiffMachine<'a> {
|
||||
}
|
||||
|
||||
pub fn diff_node(&mut self, old: &VNode<'a>, new: &VNode<'a>) {
|
||||
// pub fn diff_node(&mut self, old: &VNode<'a>, new: &VNode<'a>) {
|
||||
/*
|
||||
For each valid case, we "commit traversal", meaning we save this current position in the tree.
|
||||
Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later.
|
||||
@@ -120,7 +128,45 @@ impl<'a> DiffMachine<'a> {
|
||||
todo!("Usage of component VNode not currently supported");
|
||||
}
|
||||
|
||||
(_, VNode::Component(_)) | (VNode::Component(_), _) => {
|
||||
(_, VNode::Component(new)) => {
|
||||
// let VComponent {
|
||||
// props,
|
||||
// props_type,
|
||||
// comp,
|
||||
// caller,
|
||||
// assigned_scope,
|
||||
// ..
|
||||
// } = *new;
|
||||
|
||||
// make the component
|
||||
// let idx = unsafe {
|
||||
// // let vdom = &mut *self.vdom;
|
||||
// vdom.insert_with(|f| {
|
||||
// todo!()
|
||||
// //
|
||||
// // create_scoped(caller, props, myidx, parent)
|
||||
// })
|
||||
// };
|
||||
|
||||
// we have no stable reference to work from
|
||||
// push the lifecycle event onto the queue
|
||||
// self.lifecycle_events
|
||||
// .borrow_mut()
|
||||
// .push_back(LifecycleEvent {
|
||||
// event_type: LifecycleType::Mount {
|
||||
// props: new.props,
|
||||
// to: self.cur_idx,
|
||||
// },
|
||||
// });
|
||||
// we need to associaote this new component with a scope...
|
||||
|
||||
// self.change_list.save_known_root(id)
|
||||
self.change_list.commit_traversal();
|
||||
|
||||
// push the current
|
||||
}
|
||||
|
||||
(VNode::Component(old), _) => {
|
||||
todo!("Usage of component VNode not currently supported");
|
||||
}
|
||||
|
||||
@@ -137,7 +183,8 @@ impl<'a> DiffMachine<'a> {
|
||||
// [... node]
|
||||
//
|
||||
// The change list stack is left unchanged.
|
||||
fn diff_listeners(&mut self, old: &[Listener<'a>], new: &[Listener<'a>]) {
|
||||
fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) {
|
||||
// fn diff_listeners(&mut self, old: &[Listener<'a>], new: &[Listener<'a>]) {
|
||||
if !old.is_empty() || !new.is_empty() {
|
||||
self.change_list.commit_traversal();
|
||||
}
|
||||
|
||||
@@ -296,11 +296,13 @@ mod vtext {
|
||||
/// Virtual Components for custom user-defined components
|
||||
/// Only supports the functional syntax
|
||||
mod vcomponent {
|
||||
use crate::innerlude::{Context, FC};
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
use crate::innerlude::{Context, ScopeIdx, FC};
|
||||
use std::{any::TypeId, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
use super::DomTree;
|
||||
|
||||
pub type StableScopeAddres = Rc<RefCell<Option<ScopeIdx>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VComponent<'src> {
|
||||
_p: PhantomData<&'src ()>,
|
||||
@@ -308,6 +310,11 @@ mod vcomponent {
|
||||
pub(crate) props_type: TypeId,
|
||||
pub(crate) comp: *const (),
|
||||
pub(crate) caller: Caller,
|
||||
|
||||
// once a component gets mounted, its parent gets a stable address.
|
||||
// this way we can carry the scope index from between renders
|
||||
// genius, really!
|
||||
pub assigned_scope: StableScopeAddres,
|
||||
}
|
||||
|
||||
pub struct Caller(Box<dyn Fn(Context) -> DomTree>);
|
||||
@@ -323,10 +330,10 @@ mod vcomponent {
|
||||
// - perform comparisons when diffing (memoization)
|
||||
// -
|
||||
pub fn new<P>(comp: FC<P>, props: P) -> Self {
|
||||
let caller = move |ctx: Context| {
|
||||
let t = comp(ctx, &props);
|
||||
t
|
||||
};
|
||||
// let caller = move |ctx: Context| {
|
||||
// let t = comp(ctx, &props);
|
||||
// t
|
||||
// };
|
||||
// let _caller = comp as *const ();
|
||||
// let _props = Box::new(props);
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ pub struct EditMachine<'src> {
|
||||
pub traversal: Traversal,
|
||||
next_temporary: u32,
|
||||
forcing_new_listeners: bool,
|
||||
|
||||
pub emitter: EditList<'src>,
|
||||
}
|
||||
|
||||
@@ -214,10 +215,6 @@ impl<'a> EditMachine<'a> {
|
||||
debug_assert!(self.traversal_is_committed());
|
||||
debug_assert!(start < end);
|
||||
let temp_base = self.next_temporary;
|
||||
// debug!(
|
||||
// "emit: save_children_to_temporaries({}, {}, {})",
|
||||
// temp_base, start, end
|
||||
// );
|
||||
self.next_temporary = temp_base + (end - start) as u32;
|
||||
self.emitter.push(Edit::SaveChildrenToTemporaries {
|
||||
temp: temp_base,
|
||||
@@ -367,6 +364,11 @@ impl<'a> EditMachine<'a> {
|
||||
// debug!("emit: remove_event_listener({:?})", event);
|
||||
}
|
||||
|
||||
pub fn save_known_root(&mut self, id: ScopeIdx) {
|
||||
log::debug!("emit: save_known_root({:?})", id);
|
||||
self.emitter.push(Edit::MakeKnown { node: id })
|
||||
}
|
||||
|
||||
// pub fn save_template(&mut self, id: CacheId) {
|
||||
// debug_assert!(self.traversal_is_committed());
|
||||
// debug_assert!(!self.has_template(id));
|
||||
|
||||
+137
-142
@@ -11,22 +11,36 @@ use std::{
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
pub trait Properties: PartialEq {}
|
||||
// just for now
|
||||
impl<T: PartialEq> Properties for T {}
|
||||
|
||||
pub trait Scoped {
|
||||
fn run(&mut self);
|
||||
fn compare_props(&self, new: &dyn std::any::Any) -> bool;
|
||||
fn call_listener(&mut self, trigger: EventTrigger);
|
||||
|
||||
fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump>;
|
||||
fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump>;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub struct Scope<P: Properties> {
|
||||
// Map to the parent
|
||||
pub parent: Option<ScopeIdx>,
|
||||
|
||||
// lying, cheating reference >:(
|
||||
pub props: Box<dyn std::any::Any>,
|
||||
|
||||
// our own index
|
||||
pub myidx: ScopeIdx,
|
||||
|
||||
pub caller: FC<P>,
|
||||
|
||||
pub props: P,
|
||||
|
||||
// ==========================
|
||||
// slightly unsafe stuff
|
||||
// ==========================
|
||||
@@ -35,71 +49,113 @@ pub struct Scope {
|
||||
|
||||
// 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
|
||||
pub hooks: RefCell<Vec<*mut Hook>>,
|
||||
pub hook_arena: typed_arena::Arena<Hook>,
|
||||
|
||||
// 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
|
||||
listeners: RefCell<Vec<*const dyn Fn(crate::events::VirtualEvent)>>,
|
||||
|
||||
// Unsafety
|
||||
// - is a raw ptr because we need to compare
|
||||
pub caller: *const (),
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
// create a new scope from a function
|
||||
pub fn new<'a, P1, P2: 'static>(
|
||||
f: FC<P1>,
|
||||
props: P1,
|
||||
myidx: ScopeIdx,
|
||||
parent: Option<ScopeIdx>,
|
||||
) -> Self {
|
||||
let hook_arena = typed_arena::Arena::new();
|
||||
let hooks = RefCell::new(Vec::new());
|
||||
// instead of having it as a trait method, we use a single function
|
||||
// todo: do the unsafety magic stuff to erase the type of p
|
||||
pub fn create_scoped<P: Properties + 'static>(
|
||||
caller: FC<P>,
|
||||
props: P,
|
||||
myidx: ScopeIdx,
|
||||
parent: Option<ScopeIdx>,
|
||||
) -> Box<dyn Scoped> {
|
||||
let hook_arena = typed_arena::Arena::new();
|
||||
let hooks = RefCell::new(Vec::new());
|
||||
|
||||
// Capture the caller
|
||||
let caller = f as *const ();
|
||||
let listeners = Default::default();
|
||||
|
||||
let listeners = Default::default();
|
||||
let old_frame = BumpFrame {
|
||||
bump: Bump::new(),
|
||||
head_node: VNode::text(""),
|
||||
};
|
||||
|
||||
let old_frame = BumpFrame {
|
||||
bump: Bump::new(),
|
||||
head_node: VNode::text(""),
|
||||
let new_frame = BumpFrame {
|
||||
bump: Bump::new(),
|
||||
head_node: VNode::text(""),
|
||||
};
|
||||
|
||||
let frames = ActiveFrame::from_frames(old_frame, new_frame);
|
||||
|
||||
Box::new(Scope {
|
||||
myidx,
|
||||
hook_arena,
|
||||
hooks,
|
||||
caller,
|
||||
frames,
|
||||
listeners,
|
||||
parent,
|
||||
props,
|
||||
})
|
||||
}
|
||||
|
||||
impl<P: Properties + 'static> Scoped for Scope<P> {
|
||||
/// 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)
|
||||
fn run<'bump>(&'bump mut self) {
|
||||
let frame = {
|
||||
let frame = self.frames.next();
|
||||
frame.bump.reset();
|
||||
frame
|
||||
};
|
||||
|
||||
let new_frame = BumpFrame {
|
||||
bump: Bump::new(),
|
||||
head_node: VNode::text(""),
|
||||
let node_slot = std::rc::Rc::new(RefCell::new(None));
|
||||
|
||||
let ctx: Context<'bump> = Context {
|
||||
arena: &self.hook_arena,
|
||||
hooks: &self.hooks,
|
||||
bump: &frame.bump,
|
||||
idx: 0.into(),
|
||||
_p: PhantomData {},
|
||||
final_nodes: node_slot.clone(),
|
||||
scope: self.myidx,
|
||||
listeners: &self.listeners,
|
||||
};
|
||||
|
||||
let frames = ActiveFrame::from_frames(old_frame, new_frame);
|
||||
// Note that the actual modification of the vnode head element occurs during this call
|
||||
// let _: DomTree = caller(ctx, props);
|
||||
let _: DomTree = (self.caller)(ctx, &self.props);
|
||||
|
||||
// box the props
|
||||
let props = Box::new(props);
|
||||
/*
|
||||
SAFETY ALERT
|
||||
|
||||
let props = unsafe { std::mem::transmute::<_, Box<P2>>(props) };
|
||||
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
|
||||
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
|
||||
|
||||
Self {
|
||||
myidx,
|
||||
hook_arena,
|
||||
hooks,
|
||||
caller,
|
||||
frames,
|
||||
listeners,
|
||||
parent,
|
||||
props,
|
||||
}
|
||||
Some things to note:
|
||||
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
|
||||
- The VNode has a private API and can only be used from accessors.
|
||||
- Public API cannot drop or destructure VNode
|
||||
*/
|
||||
|
||||
frame.head_node = node_slot
|
||||
.deref()
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.expect("Viewing did not happen");
|
||||
}
|
||||
|
||||
fn compare_props(&self, new: &Any) -> bool {
|
||||
new.downcast_ref::<P>()
|
||||
.map(|f| &self.props == f)
|
||||
.expect("Props should not be of a different type")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
fn call_listener(&mut self, trigger: EventTrigger) {
|
||||
let EventTrigger {
|
||||
listener_id,
|
||||
event: source,
|
||||
@@ -126,112 +182,19 @@ impl Scope {
|
||||
self.listeners.borrow_mut().drain(..);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RawComponent {
|
||||
// used as a memoization strategy
|
||||
comparator: *const Box<dyn Fn(&Box<dyn Any>) -> bool>,
|
||||
|
||||
// used to actually run the component
|
||||
// encapsulates props
|
||||
runner: *const Box<dyn Fn(Context) -> DomTree>,
|
||||
|
||||
// the actual FC<T>
|
||||
raw: *const (),
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// 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<'bump, PLocked: Sized + 'static>(&'bump mut self) {
|
||||
let frame = {
|
||||
let frame = self.frames.next();
|
||||
frame.bump.reset();
|
||||
frame
|
||||
};
|
||||
|
||||
let node_slot = std::rc::Rc::new(RefCell::new(None));
|
||||
|
||||
let ctx: Context<'bump> = Context {
|
||||
arena: &self.hook_arena,
|
||||
hooks: &self.hooks,
|
||||
bump: &frame.bump,
|
||||
idx: 0.into(),
|
||||
_p: PhantomData {},
|
||||
final_nodes: node_slot.clone(),
|
||||
scope: self.myidx,
|
||||
listeners: &self.listeners,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
/*
|
||||
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.
|
||||
|
||||
we could do a better check to make sure that the TypeID is correct before casting
|
||||
--
|
||||
This is safe because we check that the generic type matches before casting.
|
||||
*/
|
||||
// we use plocked to be able to remove the borrowed lifetime
|
||||
// these lifetimes could be very broken, so we need to dynamically manage them
|
||||
let caller = std::mem::transmute::<*const (), FC<PLocked>>(self.caller);
|
||||
let props = self.props.downcast_ref::<PLocked>().unwrap();
|
||||
|
||||
// Note that the actual modification of the vnode head element occurs during this call
|
||||
let _: DomTree = caller(ctx, props);
|
||||
|
||||
/*
|
||||
SAFETY ALERT
|
||||
|
||||
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
|
||||
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
|
||||
|
||||
Some things to note:
|
||||
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
|
||||
- The VNode has a private API and can only be used from accessors.
|
||||
- Public API cannot drop or destructure VNode
|
||||
*/
|
||||
// the nodes we care about have been unsafely extended to a static lifetime in context
|
||||
frame.head_node = node_slot
|
||||
.deref()
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.expect("Viewing did not happen");
|
||||
}
|
||||
fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
|
||||
self.frames.current_head_node()
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_listeners(node: &VNode<'static>, listeners: &mut Vec<&Listener>) {
|
||||
if let VNode::Element(el) = *node {
|
||||
for listener in el.listeners {
|
||||
// let g = listener as *const Listener;
|
||||
listeners.push(listener);
|
||||
}
|
||||
for child in el.children {
|
||||
retrieve_listeners(child, listeners);
|
||||
}
|
||||
fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
|
||||
self.frames.prev_head_node()
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// Active-frame related code
|
||||
// ==========================
|
||||
impl Scope {
|
||||
/// Accessor to get the root node and its children (safely)\
|
||||
/// Scope is self-referntial, so we are forced to use the 'static lifetime to cheat
|
||||
pub fn new_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()
|
||||
}
|
||||
}
|
||||
|
||||
// todo, do better with the active frame stuff
|
||||
// somehow build this vnode with a lifetime tied to self
|
||||
@@ -323,8 +286,8 @@ mod tests {
|
||||
let props = ();
|
||||
let parent = None;
|
||||
let mut nodes = generational_arena::Arena::new();
|
||||
nodes.insert_with(|f| {
|
||||
let scope = Scope::new::<(), ()>(example, props, f, parent);
|
||||
nodes.insert_with(|myidx| {
|
||||
let scope = create_scoped(example, props, myidx, parent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -390,3 +353,35 @@ mod tests {
|
||||
let props = ExampleProps { name: &source_text };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(asd)]
|
||||
mod old {
|
||||
|
||||
/// The ComponentCaller struct is an opaque object that encapsultes the memoization and running functionality for FC
|
||||
///
|
||||
/// It's opaque because during the diffing mechanism, the type of props is sealed away in a closure. This makes it so
|
||||
/// scope doesn't need to be generic
|
||||
pub struct ComponentCaller {
|
||||
// used as a memoization strategy
|
||||
comparator: Box<dyn Fn(&Box<dyn Any>) -> bool>,
|
||||
|
||||
// used to actually run the component
|
||||
// encapsulates props
|
||||
runner: Box<dyn Fn(Context) -> DomTree>,
|
||||
|
||||
props_type: TypeId,
|
||||
|
||||
// the actual FC<T>
|
||||
raw: *const (),
|
||||
}
|
||||
|
||||
impl ComponentCaller {
|
||||
fn new<P>(props: P) -> Self {
|
||||
let comparator = Box::new(|f| false);
|
||||
todo!();
|
||||
// Self { comparator }
|
||||
}
|
||||
|
||||
fn update_props<P>(props: P) {}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// use crate::{changelist::EditList, nodes::VNode};
|
||||
|
||||
use crate::innerlude::*;
|
||||
use crate::{innerlude::*, scope::Properties};
|
||||
use crate::{
|
||||
patch::Edit,
|
||||
scope::{create_scoped, Scoped},
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use generational_arena::Arena;
|
||||
use std::{
|
||||
@@ -18,21 +22,37 @@ use std::{
|
||||
/// Differences are converted into patches which a renderer can use to draw the UI.
|
||||
pub struct VirtualDom {
|
||||
/// 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.
|
||||
pub(crate) components: Arena<Scope>,
|
||||
|
||||
/// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
||||
///
|
||||
/// eventually, come up with a better datastructure that reuses boxes for known P types
|
||||
/// like a generational typemap bump arena
|
||||
/// -> IE a cache line for each P type with soem heuristics on optimizing layout
|
||||
pub(crate) components: Arena<Box<dyn Scoped>>,
|
||||
// pub(crate) components: Rc<RefCell<Arena<Box<dyn Scoped>>>>,
|
||||
/// The index of the root component.
|
||||
/// Will not be ready if the dom is fresh
|
||||
base_scope: ScopeIdx,
|
||||
|
||||
event_queue: RefCell<VecDeque<LifecycleEvent>>,
|
||||
|
||||
// todo: encapsulate more state into this so we can better reuse it
|
||||
diff_bump: Bump,
|
||||
pub(crate) base_scope: ScopeIdx,
|
||||
|
||||
// Type of the original props. This is done so VirtualDom does not need to be generic.
|
||||
#[doc(hidden)]
|
||||
_root_prop_type: std::any::TypeId,
|
||||
// ======================
|
||||
// DIFF RELATED ITEMs
|
||||
// ======================
|
||||
// // todo: encapsulate more state into this so we can better reuse it
|
||||
pub(crate) diff_bump: Bump,
|
||||
// // be very very very very very careful
|
||||
// pub change_list: EditMachine<'static>,
|
||||
|
||||
// // vdom: &'a VirtualDom,
|
||||
// vdom: *mut Arena<Box<dyn Scoped>>,
|
||||
|
||||
// // vdom: Rc<RefCell<Arena<Box<dyn Scoped>>>>,
|
||||
// pub cur_idx: ScopeIdx,
|
||||
|
||||
// // todo
|
||||
// // do an indexmap sorted by height
|
||||
// dirty_nodes: fxhash::FxHashSet<ScopeIdx>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
@@ -49,55 +69,56 @@ impl VirtualDom {
|
||||
///
|
||||
/// 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: 'static>(root: FC<P>, root_props: P) -> Self {
|
||||
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
|
||||
let mut components = Arena::new();
|
||||
|
||||
let event_queue = RefCell::new(VecDeque::new());
|
||||
|
||||
// Create a reference to the component in the arena
|
||||
// Note: we are essentially running the "Mount" lifecycle event manually while the vdom doesnt yet exist
|
||||
// This puts the dom in a usable state on creation, rather than being potentially invalid
|
||||
let base_scope =
|
||||
components.insert_with(|id| Scope::new::<_, P>(root, root_props, id, None));
|
||||
let base_scope = components.insert_with(|id| create_scoped(root, root_props, id, None));
|
||||
|
||||
// evaluate the component, pushing any updates its generates into the lifecycle queue
|
||||
// todo!
|
||||
|
||||
let _root_prop_type = TypeId::of::<P>();
|
||||
let diff_bump = Bump::new();
|
||||
|
||||
Self {
|
||||
components,
|
||||
base_scope,
|
||||
event_queue,
|
||||
diff_bump,
|
||||
_root_prop_type,
|
||||
}
|
||||
todo!()
|
||||
// Self {
|
||||
// // components: RefCell::new(components),
|
||||
// components: components,
|
||||
// // components: Rc::new(RefCell::new(components)),
|
||||
// base_scope,
|
||||
// // event_queue: RefCell::new(VecDeque::new()),
|
||||
// diff_bump: Bump::new(),
|
||||
// _root_prop_type: TypeId::of::<P>(),
|
||||
// }
|
||||
}
|
||||
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom.
|
||||
///
|
||||
///
|
||||
pub fn rebuild(&mut self) -> Result<EditList<'_>> {
|
||||
|
||||
// pub fn rebuild<'s>(&'s mut self) -> Result<> {
|
||||
// pub fn rebuild<'s>(&'s mut self) -> Result<std::cell::Ref<'_, Arena<Box<dyn Scoped>>>> {
|
||||
pub fn rebuild<'s>(&'s mut self) -> Result<EditList<'s>> {
|
||||
// Reset and then build a new diff machine
|
||||
// The previous edit list cannot be around while &mut is held
|
||||
// Make sure variance doesnt break this
|
||||
self.diff_bump.reset();
|
||||
let mut diff_machine = DiffMachine::new(&self.diff_bump);
|
||||
|
||||
// this is still a WIP
|
||||
// we'll need to re-fecth all the scopes that were changed and build the diff machine
|
||||
// fetch the component again
|
||||
let component = self
|
||||
.components
|
||||
self.components
|
||||
.get_mut(self.base_scope)
|
||||
.expect("Root should always exist");
|
||||
.expect("Root should always exist")
|
||||
.run();
|
||||
|
||||
component.run::<()>();
|
||||
let b = Bump::new();
|
||||
|
||||
diff_machine.diff_node(component.old_frame(), component.new_frame());
|
||||
let mut diff_machine = DiffMachine::new(self, &b, self.base_scope);
|
||||
// let mut diff_machine = DiffMachine::new(self, &self.diff_bump, self.base_scope);
|
||||
|
||||
Ok(diff_machine.consume())
|
||||
todo!()
|
||||
// let component = self.components.get(self.base_scope).unwrap();
|
||||
|
||||
// diff_machine.diff_node(component.old_frame(), component.new_frame());
|
||||
|
||||
// let edits = diff_machine.consume();
|
||||
|
||||
// self.diff_bump = b;
|
||||
|
||||
// Ok(edits)
|
||||
}
|
||||
|
||||
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
|
||||
@@ -125,37 +146,32 @@ impl VirtualDom {
|
||||
///
|
||||
/// ```
|
||||
pub fn progress_with_event(&mut self, event: EventTrigger) -> Result<EditList<'_>> {
|
||||
let component = self
|
||||
.components
|
||||
.get_mut(event.component_id)
|
||||
.expect("Component should exist if an event was triggered");
|
||||
// self.components
|
||||
// .borrow_mut()
|
||||
// .get_mut(event.component_id)
|
||||
// .map(|f| {
|
||||
// f.call_listener(event);
|
||||
// f
|
||||
// })
|
||||
// .map(|f| f.run())
|
||||
// .expect("Borrowing should not fail");
|
||||
|
||||
component.call_listener(event);
|
||||
// component.call_listener(event);
|
||||
|
||||
// .expect("Component should exist if an event was triggered");
|
||||
// Reset and then build a new diff machine
|
||||
// The previous edit list cannot be around while &mut is held
|
||||
// Make sure variance doesnt break this
|
||||
self.diff_bump.reset();
|
||||
let mut diff_machine = DiffMachine::new(&self.diff_bump);
|
||||
// self.diff_bump.reset();
|
||||
// let mut diff_machine = DiffMachine::new(&mut self, event.component_id);
|
||||
// let mut diff_machine =
|
||||
// DiffMachine::new(&self.diff_bump, &mut self.components, event.component_id);
|
||||
|
||||
// this is still a WIP
|
||||
// we'll need to re-fecth all the scopes that were changed and build the diff machine
|
||||
// fetch the component again
|
||||
// let component = self
|
||||
// .components
|
||||
// .get_mut(self.base_scope)
|
||||
// .expect("Root should always exist");
|
||||
// component.run();
|
||||
// diff_machine.diff_node(component.old_frame(), component.new_frame());
|
||||
|
||||
component.run::<()>();
|
||||
|
||||
diff_machine.diff_node(component.old_frame(), component.new_frame());
|
||||
// diff_machine.diff_node(
|
||||
// component.old_frame(),
|
||||
// component.new_frame(),
|
||||
// Some(self.base_scope),
|
||||
// );
|
||||
|
||||
Ok(diff_machine.consume())
|
||||
todo!()
|
||||
// Ok(diff_machine.consume())
|
||||
// Err(crate::error::Error::NoEvent)
|
||||
// Mark dirty components. Descend from the highest node until all dirty nodes are updated.
|
||||
// let mut affected_components = Vec::new();
|
||||
@@ -169,106 +185,8 @@ impl VirtualDom {
|
||||
|
||||
// todo!()
|
||||
}
|
||||
|
||||
/// Using mutable access to the Virtual Dom, progress a given lifecycle event
|
||||
fn process_lifecycle(&mut self, LifecycleEvent { event_type }: LifecycleEvent) -> Result<()> {
|
||||
match event_type {
|
||||
// Component needs to be mounted to the virtual dom
|
||||
LifecycleType::Mount {
|
||||
to: _,
|
||||
under: _,
|
||||
props: _,
|
||||
} => {}
|
||||
|
||||
// The parent for this component generated new props and the component needs update
|
||||
LifecycleType::PropsChanged {
|
||||
props: _,
|
||||
component: _,
|
||||
} => {}
|
||||
|
||||
// Component was messaged via the internal subscription service
|
||||
LifecycleType::Callback { component: _ } => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pop the top event of the internal lifecycle event queu
|
||||
pub fn pop_event(&self) -> Option<LifecycleEvent> {
|
||||
self.event_queue.borrow_mut().pop_front()
|
||||
}
|
||||
|
||||
/// With access to the virtual dom, schedule an update to the Root component's props.
|
||||
/// This generates the appropriate Lifecycle even. It's up to the renderer to actually feed this lifecycle event
|
||||
/// back into the event system to get an edit list.
|
||||
pub fn update_props<P: 'static>(&mut self, new_props: P) -> Result<LifecycleEvent> {
|
||||
// Ensure the props match
|
||||
if TypeId::of::<P>() != self._root_prop_type {
|
||||
return Err(Error::WrongProps);
|
||||
}
|
||||
|
||||
Ok(LifecycleEvent {
|
||||
event_type: LifecycleType::PropsChanged {
|
||||
props: Box::new(new_props),
|
||||
component: self.base_scope,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LifecycleEvent {
|
||||
pub event_type: LifecycleType,
|
||||
}
|
||||
|
||||
pub enum LifecycleType {
|
||||
// Component needs to be mounted, but its scope doesn't exist yet
|
||||
Mount {
|
||||
to: ScopeIdx,
|
||||
under: usize,
|
||||
props: Box<dyn std::any::Any>,
|
||||
},
|
||||
|
||||
// Parent was evalauted causing new props to generate
|
||||
PropsChanged {
|
||||
props: Box<dyn std::any::Any>,
|
||||
component: ScopeIdx,
|
||||
},
|
||||
|
||||
// Hook for the subscription API
|
||||
Callback {
|
||||
component: ScopeIdx,
|
||||
},
|
||||
}
|
||||
|
||||
impl LifecycleEvent {
|
||||
fn index(&self) -> Option<ScopeIdx> {
|
||||
match &self.event_type {
|
||||
LifecycleType::Mount {
|
||||
to: _,
|
||||
under: _,
|
||||
props: _,
|
||||
} => None,
|
||||
|
||||
LifecycleType::PropsChanged { component, .. }
|
||||
| LifecycleType::Callback { component } => Some(component.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_dom() {
|
||||
let mut dom = VirtualDom::new(|ctx, props| {
|
||||
todo!()
|
||||
// ctx.render(|ctx| {
|
||||
// use crate::builder::*;
|
||||
// let bump = ctx.bump();
|
||||
// div(bump).child(text("hello, world")).finish()
|
||||
// })
|
||||
});
|
||||
let edits = dom.rebuild().unwrap();
|
||||
println!("{:#?}", edits);
|
||||
}
|
||||
}
|
||||
// struct LockedEdits<'src> {
|
||||
// edits:
|
||||
// }
|
||||
Reference in new issue
Block a user