dioxus/packages/core/src/virtual_dom.rs

262 lines
8.3 KiB
Rust
Raw Normal View History

2021-02-03 07:26:04 +00:00
use crate::nodes::VNode;
2021-02-12 21:11:33 +00:00
use crate::{events::EventTrigger, innerlude::*};
2021-02-07 21:21:38 +00:00
use any::Any;
2021-02-03 07:26:04 +00:00
use bumpalo::Bump;
use generational_arena::{Arena, Index};
use std::{
2021-02-07 21:21:38 +00:00
any::{self, TypeId},
cell::{RefCell, UnsafeCell},
future::Future,
2021-02-07 19:59:34 +00:00
marker::PhantomData,
rc::Rc,
sync::atomic::AtomicUsize,
};
2021-02-03 07:26:04 +00:00
/// 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<P: Properties> {
2021-02-03 07:26:04 +00:00
/// 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>,
2021-02-03 07:26:04 +00:00
2021-02-07 22:38:17 +00:00
/// The index of the root component.
base_scope: Index,
///
///
///
///
event_queue: Rc<RefCell<Vec<LifecycleEvent>>>,
2021-02-03 07:26:04 +00:00
#[doc(hidden)] // mark the root props with P, even though they're held by the root component
_root_props: PhantomData<P>,
2021-02-03 07:26:04 +00:00
}
impl VirtualDom<()> {
2021-02-03 07:26:04 +00:00
/// 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, ())
2021-02-03 07:26:04 +00:00
}
}
2021-02-03 07:26:04 +00:00
impl<P: Properties + 'static> VirtualDom<P> {
2021-02-03 07:26:04 +00:00
/// 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(root: FC<P>, root_props: P) -> Self {
2021-02-07 22:38:17 +00:00
// 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
let mut components = Arena::new();
// Create a reference to the component in the arena
let base_scope = components.insert(Scope::new(root, None));
2021-02-07 22:38:17 +00:00
// Create a new mount event with no root container
let first_event = LifecycleEvent::mount(base_scope, None, 0, root_props);
2021-02-07 22:38:17 +00:00
// Create an event queue with a mount for the base scope
let event_queue = Rc::new(RefCell::new(vec![first_event]));
2021-02-03 07:26:04 +00:00
Self {
components,
base_scope,
event_queue,
_root_props: PhantomData {},
2021-02-03 07:26:04 +00:00
}
}
2021-02-12 21:11:33 +00:00
/// With access to the virtual dom, schedule an update to the Root component's props
pub fn update_props(&mut self, new_props: P) {
self.event_queue.borrow_mut().push(LifecycleEvent {
2021-02-12 21:11:33 +00:00
event_type: LifecycleType::PropsChanged {
props: Box::new(new_props),
},
index: self.base_scope,
});
}
/// Pop an event off the event queue and process it
2021-02-12 21:11:33 +00:00
/// Update the root props, and progress
/// Takes a bump arena to allocate into, making the diff phase as fast as possible
///
pub fn progress(&mut self) -> Result<()> {
let event = self.event_queue.borrow_mut().pop().ok_or(Error::NoEvent)?;
process_event(&mut self.components, event)
}
2021-02-03 07:26:04 +00:00
2021-02-12 21:11:33 +00:00
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
///
/// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual
/// dom to completion, tagging components that need updates, compressing events together, and finally emitting a single
/// change list.
///
/// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on
/// listeners.
///
/// ```ignore
///
///
///
///
/// ```
pub async fn progress_with_event(&mut self, evt: EventTrigger) -> Result<()> {
let EventTrigger {
component_id,
listener_id,
event,
} = evt;
let component = self
.components
.get(component_id)
// todo: update this with a dedicated error type so implementors know what went wrong
.expect("Component should exist if an event was triggered");
let listener = component
.listeners
.get(listener_id as usize)
.expect("Listener should exist if it was triggered")
.as_ref();
// Run the callback
// This should cause internal state to progress, dumping events into the event queue
// todo: integrate this with a tracing mechanism exposed to a dev tool
2021-02-12 21:11:33 +00:00
listener();
// Run through our events, tagging which Indexes are receiving updates
// Prop updates take prescedence over subscription updates
// Run all prop updates *first* as they will cascade into children.
// *then* run the non-prop updates that were not already covered by props
let mut events = self.event_queue.borrow_mut();
// for now, just naively process each event in the queue
for event in events.drain(..) {
process_event(&mut self.components, event)?;
}
2021-02-12 21:11:33 +00:00
Ok(())
}
pub async fn progress_completely(&mut self) -> Result<()> {
Ok(())
}
}
/// Using mutable access to the Virtual Dom, progress a given lifecycle event
///
///
///
///
///
///
fn process_event(
// dom: &mut VirtualDom<P>,
components: &mut Arena<Scope>,
LifecycleEvent { index, event_type }: LifecycleEvent,
) -> Result<()> {
let scope = components.get(index).ok_or(Error::NoEvent)?;
match event_type {
// Component needs to be mounted to the virtual dom
LifecycleType::Mount { to, under, props } => {
if let Some(other) = to {
// mount to another component
} else {
// mount to the root
}
let g = props.as_ref();
scope.run(g);
// scope.run(runner, props, dom);
}
// The parent for this component generated new props and the component needs update
LifecycleType::PropsChanged { props } => {
//
}
// 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 = components.remove(index);
// let f = dom.components.remove(index);
}
// Component was messaged via the internal subscription service
LifecycleType::Messaged => {
//
}
// Event from renderer was fired with a given listener ID
//
LifecycleType::Callback { listener_id } => {}
2021-02-12 21:11:33 +00:00
// Run any post-render callbacks on a component
LifecycleType::Rendered => {}
}
Ok(())
2021-02-03 07:26:04 +00:00
}
pub struct LifecycleEvent {
pub index: Index,
pub event_type: LifecycleType,
}
/// The internal lifecycle event system is managed by these
/// Right now, we box the properties and but them in the enum
/// Later, we could directly call the chain of children without boxing
/// We could try to reuse the boxes somehow
pub enum LifecycleType {
Mount {
to: Option<Index>,
under: usize,
props: Box<dyn Properties>,
},
PropsChanged {
props: Box<dyn Properties>,
},
2021-02-12 21:11:33 +00:00
Rendered,
Mounted,
Removed,
Messaged,
Callback {
listener_id: i32,
},
}
impl LifecycleEvent {
// helper method for shortcutting to the enum type
// probably not necessary
fn mount<P: Properties + 'static>(
which: Index,
to: Option<Index>,
under: usize,
props: P,
) -> Self {
Self {
index: which,
event_type: LifecycleType::Mount {
to,
under,
props: Box::new(props),
},
}
}
}