dioxus/packages/core/src/virtual_dom.rs

421 lines
15 KiB
Rust
Raw Normal View History

2021-05-16 06:06:02 +00:00
//! # VirtualDOM Implementation for Rust
2021-09-10 00:58:48 +00:00
//!
2021-05-16 06:06:02 +00:00
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
//!
//! In this file, multiple items are defined. This file is big, but should be documented well to
//! navigate the innerworkings of the Dom. We try to keep these main mechanics in this file to limit
//! the possible exposed API surface (keep fields private). This particular implementation of VDOM
//! is extremely efficient, but relies on some unsafety under the hood to do things like manage
2021-05-18 05:16:43 +00:00
//! micro-heaps for components. We are currently working on refactoring the safety out into safe(r)
//! abstractions, but current tests (MIRI and otherwise) show no issues with the current implementation.
2021-05-16 06:06:02 +00:00
//!
//! Included is:
//! - The [`VirtualDom`] itself
//! - The [`Scope`] object for mangning component lifecycle
//! - The [`ActiveFrame`] object for managing the Scope`s microheap
//! - The [`Context`] object for exposing VirtualDOM API to components
2021-07-01 18:14:59 +00:00
//! - The [`NodeFactory`] object for lazyily exposing the `Context` API to the nodebuilder API
2021-05-16 06:06:02 +00:00
//!
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
2021-08-27 13:40:04 +00:00
use crate::innerlude::*;
2021-10-01 06:07:12 +00:00
use std::{any::Any, rc::Rc, sync::Arc};
2021-07-09 05:36:18 +00:00
2021-02-03 07:26:04 +00:00
/// An integrated virtual node system that progresses events and diffs UI trees.
///
2021-09-10 00:58:48 +00:00
/// Differences are converted into patches which a renderer can use to draw the UI.
///
2021-09-10 00:58:48 +00:00
/// If you are building an App with Dioxus, you probably won't want to reach for this directly, instead opting to defer
/// to a particular crate's wrapper over the [`VirtualDom`] API.
///
2021-09-10 00:58:48 +00:00
/// Example
/// ```rust
2021-09-21 17:42:52 +00:00
/// static App: FC<()> = |cx, props|{
2021-09-10 00:58:48 +00:00
/// cx.render(rsx!{
/// div {
/// "Hello World"
/// }
/// })
/// }
///
2021-09-10 00:58:48 +00:00
/// async fn main() {
/// let mut dom = VirtualDom::new(App);
/// let mut inital_edits = dom.rebuild();
/// initialize_screen(inital_edits);
///
2021-09-10 00:58:48 +00:00
/// loop {
/// let next_frame = TimeoutFuture::new(Duration::from_millis(16));
/// let edits = dom.run_with_deadline(next_frame).await;
/// apply_edits(edits);
/// render_frame();
/// }
/// }
/// ```
pub struct VirtualDom {
2021-08-27 13:40:04 +00:00
scheduler: Scheduler,
2021-08-09 21:09:33 +00:00
base_scope: ScopeId,
2021-03-29 16:31:47 +00:00
2021-09-01 19:45:53 +00:00
root_fc: Box<dyn Any>,
2021-10-01 06:07:12 +00:00
root_props: Rc<dyn Any + Send>,
// we need to keep the allocation around, but we don't necessarily use it
2021-10-01 06:07:12 +00:00
_root_caller: RootCaller,
2021-02-03 07:26:04 +00:00
}
impl VirtualDom {
/// Create a new VirtualDOM with a component that does not have special props.
2021-05-16 06:06:02 +00:00
///
/// # Description
2021-05-16 06:06:02 +00:00
///
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
2021-05-16 06:06:02 +00:00
///
/// 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.
2021-05-16 06:06:02 +00:00
///
///
/// # Example
/// ```
2021-08-27 13:40:04 +00:00
/// fn Example(cx: Context<()>) -> DomTree {
/// cx.render(rsx!( div { "hello world" } ))
2021-05-16 06:06:02 +00:00
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
///
/// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
2021-06-23 05:44:48 +00:00
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
2021-02-03 07:26:04 +00:00
}
/// Create a new VirtualDOM with the given properties for the root component.
///
/// # Description
///
2021-02-03 07:26:04 +00:00
/// 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.
2021-05-16 06:06:02 +00:00
///
///
/// # Example
/// ```
2021-08-27 13:40:04 +00:00
/// #[derive(PartialEq, Props)]
/// struct SomeProps {
/// name: &'static str
/// }
///
/// fn Example(cx: Context<SomeProps>) -> DomTree {
/// cx.render(rsx!{ div{ "hello {cx.name}" } })
2021-05-16 06:06:02 +00:00
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
///
2021-08-27 13:40:04 +00:00
/// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
///
/// ```rust
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
2021-10-01 06:07:12 +00:00
pub fn new_with_props<P: 'static + Send>(root: FC<P>, root_props: P) -> Self {
2021-09-13 05:14:09 +00:00
let root_fc = Box::new(root);
2021-10-01 06:07:12 +00:00
let root_props: Rc<dyn Any + Send> = Rc::new(root_props);
2021-06-07 18:14:49 +00:00
2021-10-01 06:07:12 +00:00
let _p = root_props.clone();
2021-09-13 16:42:38 +00:00
// Safety: this callback is only valid for the lifetime of the root props
2021-10-01 06:07:12 +00:00
let root_caller: Rc<dyn Fn(&Scope) -> DomTree> = Rc::new(move |scope: &Scope| unsafe {
let props = _p.downcast_ref::<P>().unwrap();
2021-09-21 17:13:15 +00:00
std::mem::transmute(root(Context { scope }, props))
});
2021-09-13 05:14:09 +00:00
let scheduler = Scheduler::new();
2021-08-26 21:05:28 +00:00
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
2021-08-25 20:40:18 +00:00
Scope::new(
root_caller.as_ref(),
2021-08-25 20:40:18 +00:00
myidx,
None,
0,
ScopeChildren(&[]),
2021-08-27 02:05:09 +00:00
scheduler.pool.channel.clone(),
2021-08-25 20:40:18 +00:00
)
});
2021-02-07 22:38:17 +00:00
2021-03-11 17:27:01 +00:00
Self {
2021-10-01 06:07:12 +00:00
_root_caller: RootCaller(root_caller),
2021-09-13 05:14:09 +00:00
root_fc,
2021-05-16 06:06:02 +00:00
base_scope,
2021-08-25 20:40:18 +00:00
scheduler,
2021-09-01 19:52:38 +00:00
root_props,
2021-03-11 17:27:01 +00:00
}
}
2021-02-03 07:26:04 +00:00
2021-09-01 19:52:38 +00:00
/// Get the [`Scope`] for the root component.
///
/// This is useful for traversing the tree from the root for heuristics or altnerative renderers that use Dioxus
/// directly.
2021-08-06 02:23:41 +00:00
pub fn base_scope(&self) -> &Scope {
2021-08-26 21:05:28 +00:00
self.scheduler.pool.get_scope(self.base_scope).unwrap()
2021-08-06 02:23:41 +00:00
}
2021-09-01 19:52:38 +00:00
/// Get the [`Scope`] for a component given its [`ScopeId`]
2021-08-06 02:23:41 +00:00
pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
2021-08-26 21:05:28 +00:00
self.scheduler.pool.get_scope(id)
2021-08-06 02:23:41 +00:00
}
2021-09-01 19:45:53 +00:00
/// Update the root props of this VirtualDOM.
///
/// This method retuns None if the old props could not be removed. The entire VirtualDOM will be rebuilt immediately,
/// so calling this method will block the main thread until computation is done.
2021-09-01 19:52:38 +00:00
///
/// ## Example
///
/// ```rust
/// #[derive(Props, PartialEq)]
/// struct AppProps {
/// route: &'static str
/// }
2021-09-21 17:42:52 +00:00
/// static App: FC<AppProps> = |cx, props|cx.render(rsx!{ "route is {cx.route}" });
2021-09-01 19:52:38 +00:00
///
/// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" });
///
/// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap();
/// ```
2021-09-21 22:13:09 +00:00
pub fn update_root_props<P>(&mut self, root_props: P) -> Option<Mutations>
where
2021-10-01 06:07:12 +00:00
P: 'static + Send,
2021-09-21 22:13:09 +00:00
{
2021-09-01 19:45:53 +00:00
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
// Pre-emptively drop any downstream references of the old props
2021-09-01 19:45:53 +00:00
root_scope.ensure_drop_safety(&self.scheduler.pool);
2021-10-01 06:07:12 +00:00
let mut root_props: Rc<dyn Any + Send> = Rc::new(root_props);
2021-09-01 19:45:53 +00:00
2021-09-01 19:52:38 +00:00
if let Some(props_ptr) = root_props.downcast_ref::<P>().map(|p| p as *const P) {
// Swap the old props and new props
2021-09-01 19:52:38 +00:00
std::mem::swap(&mut self.root_props, &mut root_props);
2021-09-01 19:45:53 +00:00
let root = *self.root_fc.downcast_ref::<FC<P>>().unwrap();
2021-09-13 16:42:38 +00:00
let root_caller: Box<dyn Fn(&Scope) -> DomTree> =
Box::new(move |scope: &Scope| unsafe {
let props: &'_ P = &*(props_ptr as *const P);
2021-09-21 17:13:15 +00:00
std::mem::transmute(root(Context { scope }, props))
2021-09-13 16:42:38 +00:00
});
2021-09-01 19:45:53 +00:00
2021-09-13 16:42:38 +00:00
root_scope.update_scope_dependencies(&root_caller, ScopeChildren(&[]));
2021-09-01 19:45:53 +00:00
drop(root_props);
2021-09-01 19:45:53 +00:00
Some(self.rebuild())
} else {
None
}
}
2021-09-10 00:58:48 +00:00
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch
///
2021-09-10 00:58:48 +00:00
/// The diff machine expects the RealDom's stack to be the root of the application.
2021-08-06 02:23:41 +00:00
///
2021-09-10 00:58:48 +00:00
/// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
/// root component will be ran once and then diffed. All updates will flow out as mutations.
///
/// All state stored in components will be completely wiped away.
///
2021-09-10 00:58:48 +00:00
/// # Example
/// ```
2021-09-21 17:42:52 +00:00
/// static App: FC<()> = |cx, props|cx.render(rsx!{ "hello world" });
2021-09-10 00:58:48 +00:00
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
2021-08-24 19:12:20 +00:00
///
2021-09-10 00:58:48 +00:00
/// apply_edits(edits);
/// ```
2021-09-21 22:13:09 +00:00
pub fn rebuild(&mut self) -> Mutations {
self.scheduler.rebuild(self.base_scope)
2021-08-24 19:12:20 +00:00
}
2021-09-02 03:33:25 +00:00
/// Compute a manual diff of the VirtualDOM between states.
///
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event.
2021-09-13 05:14:09 +00:00
///
/// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used
/// to force an update of the DOM when the state of the app is changed outside of the app.
///
///
/// # Example
/// ```rust
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
/// }
///
2021-09-21 17:42:52 +00:00
/// static App: FC<AppProps> = |cx, props|{
2021-09-13 05:14:09 +00:00
/// let val = cx.value.borrow();
/// cx.render(rsx! { div { "{val}" } })
/// };
///
/// let value = Rc::new(RefCell::new("Hello"));
/// let mut dom = VirtualDom::new_with_props(
/// App,
/// AppProps {
/// value: value.clone(),
/// },
/// );
///
/// let _ = dom.rebuild();
///
/// *value.borrow_mut() = "goodbye";
///
/// let edits = dom.diff();
/// ```
2021-09-21 22:13:09 +00:00
pub fn diff(&mut self) -> Mutations {
self.scheduler.hard_diff(self.base_scope)
2021-08-21 17:24:47 +00:00
}
2021-08-22 21:08:25 +00:00
2021-08-08 19:15:16 +00:00
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
///
2021-09-02 03:22:34 +00:00
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
2021-09-21 22:13:09 +00:00
pub fn run_immediate(&mut self) -> Option<Vec<Mutations>> {
2021-09-02 03:22:34 +00:00
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
} else {
None
}
2021-08-08 19:15:16 +00:00
}
2021-08-06 02:23:41 +00:00
2021-08-09 21:09:33 +00:00
/// Run the virtualdom with a deadline.
2021-08-08 19:15:16 +00:00
///
/// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
/// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
/// exhaust the deadline working on them.
///
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
///
2021-08-09 17:17:19 +00:00
/// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
/// deadline closure manually.
2021-08-08 19:15:16 +00:00
///
2021-08-09 21:09:33 +00:00
/// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking
2021-08-08 19:15:16 +00:00
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
/// the screen will "jank" up. In debug, this will trigger an alert.
///
2021-08-09 17:17:19 +00:00
/// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
/// the provided deadline future resolves.
///
/// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
/// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
/// entirely jank-free applications that perform a ton of work.
///
2021-08-08 19:15:16 +00:00
/// # Example
///
/// ```no_run
2021-09-21 17:42:52 +00:00
/// static App: FC<()> = |cx, props|rsx!(cx, div {"hello"} );
2021-08-09 17:17:19 +00:00
/// let mut dom = VirtualDom::new(App);
2021-08-08 19:15:16 +00:00
/// loop {
2021-08-09 17:17:19 +00:00
/// let deadline = TimeoutFuture::from_ms(16);
2021-08-08 19:15:16 +00:00
/// let mutations = dom.run_with_deadline(deadline).await;
/// apply_mutations(mutations);
/// }
/// ```
2021-08-09 21:09:33 +00:00
///
/// ## Mutations
///
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
2021-09-22 05:25:28 +00:00
pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec<Mutations<'_>> {
self.scheduler.work_with_deadline(deadline)
2021-05-15 16:03:08 +00:00
}
2021-06-15 14:02:46 +00:00
2021-08-25 20:40:18 +00:00
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
2021-08-27 02:05:09 +00:00
self.scheduler.pool.channel.sender.clone()
}
2021-08-24 20:29:10 +00:00
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
2021-10-01 06:07:12 +00:00
todo!("making vdom send right now");
// if self.scheduler.has_any_work() {
// log::debug!("No need to wait for work, we already have some");
// return;
// }
// log::debug!("No active work.... waiting for some...");
// use futures_util::StreamExt;
// // right now this won't poll events if there is ongoing work
// // in the future we want to prioritize some events over ongoing work
// // this is coming in the "priorities" PR
// // Wait for any new events if we have nothing to do
// // todo: poll the events once even if there is work to do to prevent starvation
// futures_util::select! {
// _ = self.scheduler.async_tasks.next() => {}
// msg = self.scheduler.receiver.next() => {
// match msg.unwrap() {
// SchedulerMsg::Task(t) => todo!(),
// SchedulerMsg::Immediate(im) => {
// self.scheduler.dirty_scopes.insert(im);
// }
// SchedulerMsg::UiEvent(evt) => {
// self.scheduler.ui_events.push_back(evt);
// }
// }
// },
// }
// while let Ok(Some(msg)) = self.scheduler.receiver.try_next() {
// match msg {
// SchedulerMsg::Task(t) => todo!(),
// SchedulerMsg::Immediate(im) => {
// self.scheduler.dirty_scopes.insert(im);
// }
// SchedulerMsg::UiEvent(evt) => {
// self.scheduler.ui_events.push_back(evt);
// }
// }
// }
}
2021-04-05 01:47:53 +00:00
}
2021-09-13 23:07:15 +00:00
impl std::fmt::Display for VirtualDom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let base = self.base_scope();
let root = base.root_node();
let renderer = ScopeRenderer {
show_fragments: false,
skip_components: false,
_scope: base,
_pre_render: false,
_newline: true,
_indent: true,
_max_depth: usize::MAX,
};
renderer.render(self, root, f, 0)
}
}
2021-10-01 06:07:12 +00:00
/*
Send safety...
The VirtualDom can only be "send" if the internals exposed to user code are also "send".
IE it's okay to move an Rc from one thread to another thread as long as it's only used internally
*/
// we never actually use the contents of this root caller
struct RootCaller(Rc<dyn for<'b> Fn(&'b Scope) -> DomTree<'b> + 'static>);