dioxus/packages/core/src/virtual_dom.rs

790 lines
29 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
2021-10-24 17:30:36 +00:00
//! navigate the inner workings of the Dom. We try to keep these main mechanics in this file to limit
2021-05-16 06:06:02 +00:00
//! 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
2021-10-24 17:30:36 +00:00
//! - The [`Scope`] object for managing component lifecycle
2021-05-16 06:06:02 +00:00
//! - The [`ActiveFrame`] object for managing the Scope`s microheap
//! - The [`Context`] object for exposing VirtualDOM API to components
2021-10-24 17:30:36 +00:00
//! - The [`NodeFactory`] object for lazily 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
2021-11-05 20:28:08 +00:00
use crate::innerlude::*;
use bumpalo::Bump;
2021-10-04 05:28:04 +00:00
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
2021-11-05 20:28:08 +00:00
use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt};
use fxhash::FxHashMap;
use fxhash::FxHashSet;
use indexmap::IndexSet;
use slab::Slab;
use std::pin::Pin;
use std::task::Poll;
use std::{
any::{Any, TypeId},
cell::{Cell, UnsafeCell},
collections::{HashSet, VecDeque},
rc::Rc,
};
2021-10-04 05:28:04 +00:00
use crate::innerlude::*;
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-10-16 21:37:28 +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-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>,
root_props: Rc<dyn Any>,
// we need to keep the allocation around, but we don't necessarily use it
_root_caller: Box<dyn Any>,
2021-11-05 20:28:08 +00:00
pub(crate) scopes: ScopeArena,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
receiver: UnboundedReceiver<SchedulerMsg>,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
2021-11-05 20:28:08 +00:00
// Every component that has futures that need to be polled
2021-11-07 03:11:17 +00:00
pending_futures: FxHashSet<ScopeId>,
pending_messages: VecDeque<SchedulerMsg>,
dirty_scopes: IndexSet<ScopeId>,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
saved_state: Option<SavedDiffWork<'static>>,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
in_progress: bool,
2021-02-03 07:26:04 +00:00
}
2021-11-07 03:11:17 +00:00
// Methods to create the VirtualDom
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-10-04 05:28:04 +00:00
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
Self::new_with_props_and_scheduler(root, root_props, sender, receiver)
}
2021-11-05 22:02:44 +00:00
/// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler
2021-10-04 05:28:04 +00:00
///
/// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the
2021-10-24 17:30:36 +00:00
/// VirtualDom to be created just to retrieve its channel receiver.
pub fn new_with_props_and_scheduler<P: 'static>(
2021-10-04 05:28:04 +00:00
root: FC<P>,
root_props: P,
sender: UnboundedSender<SchedulerMsg>,
receiver: UnboundedReceiver<SchedulerMsg>,
) -> Self {
let mut scopes = ScopeArena::new();
let base_scope = scopes.new_with_key(
//
root as _,
2021-11-05 22:02:44 +00:00
todo!(),
// boxed_comp.as_ref(),
None,
0,
0,
sender.clone(),
);
2021-11-05 20:28:08 +00:00
2021-03-11 17:27:01 +00:00
Self {
scopes,
2021-11-05 22:02:44 +00:00
base_scope,
receiver,
sender,
2021-11-05 20:28:08 +00:00
root_fc: todo!(),
root_props: todo!(),
_root_caller: todo!(),
2021-11-05 22:02:44 +00:00
2021-11-07 03:11:17 +00:00
pending_messages: VecDeque::new(),
2021-11-05 22:02:44 +00:00
pending_futures: Default::default(),
dirty_scopes: Default::default(),
2021-11-07 03:11:17 +00:00
saved_state: None,
2021-11-05 22:02:44 +00:00
in_progress: false,
2021-03-11 17:27:01 +00:00
}
}
2021-11-07 03:11:17 +00:00
}
2021-02-03 07:26:04 +00:00
2021-11-07 03:11:17 +00:00
// Public utility methods
impl VirtualDom {
/// Get the [`ScopeState`] for the root component.
2021-09-01 19:52:38 +00:00
///
2021-10-24 17:30:36 +00:00
/// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus
2021-09-01 19:52:38 +00:00
/// directly.
2021-11-07 03:11:17 +00:00
///
/// # Example
pub fn base_scope(&self) -> &ScopeState {
self.get_scope(&self.base_scope).unwrap()
2021-08-06 02:23:41 +00:00
}
2021-11-07 03:11:17 +00:00
/// Get the [`ScopeState`] for a component given its [`ScopeId`]
///
/// # Example
///
///
///
pub fn get_scope<'a>(&'a self, id: &ScopeId) -> Option<&'a ScopeState> {
self.scopes.get(&id)
2021-08-06 02:23:41 +00:00
}
2021-11-07 03:11:17 +00:00
/// Get an [`UnboundedSender`] handle to the channel used by the scheduler.
///
/// # Example
2021-09-01 19:45:53 +00:00
///
2021-09-01 19:52:38 +00:00
///
2021-11-07 03:11:17 +00:00
///
pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
self.sender.clone()
}
/// Check if the [`VirtualDom`] has any pending updates or work to be done.
2021-09-01 19:52:38 +00:00
///
2021-11-07 03:11:17 +00:00
/// # Example
2021-09-01 19:52:38 +00:00
///
///
2021-11-07 03:11:17 +00:00
///
pub fn has_any_work(&self) -> bool {
!(self.dirty_scopes.is_empty() && self.pending_messages.is_empty())
2021-09-01 19:45:53 +00:00
}
2021-11-07 03:11:17 +00:00
}
2021-09-01 19:45:53 +00:00
2021-11-07 03:11:17 +00:00
// Methods to actually run the VirtualDOM
impl VirtualDom {
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-11-07 03:11:17 +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 {
2021-11-07 03:11:17 +00:00
self.hard_diff(&self.base_scope)
2021-08-21 17:24:47 +00:00
}
2021-08-22 21:08:25 +00:00
2021-11-07 03:11:17 +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) {
// todo: poll the events once even if there is work to do to prevent starvation
2021-11-05 20:28:08 +00:00
if self.has_any_work() {
2021-11-07 03:11:17 +00:00
return;
}
struct PollTasks<'a> {
pending_futures: &'a FxHashSet<ScopeId>,
scopes: &'a ScopeArena,
}
impl<'a> Future for PollTasks<'a> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut all_pending = true;
// Poll every scope manually
for fut in self.pending_futures.iter() {
let scope = self.scopes.get(fut).expect("Scope should never be moved");
let mut items = scope.items.borrow_mut();
for task in items.tasks.iter_mut() {
let task = task.as_mut();
// todo: does this make sense?
// I don't usually write futures by hand
let unpinned = unsafe { Pin::new_unchecked(task) };
match unpinned.poll(cx) {
Poll::Ready(_) => {
all_pending = false;
}
Poll::Pending => {}
}
}
}
// Resolve the future if any task is ready
match all_pending {
true => Poll::Pending,
false => Poll::Ready(()),
}
}
}
let scheduler_fut = self.receiver.next();
let tasks_fut = PollTasks {
pending_futures: &self.pending_futures,
scopes: &self.scopes,
};
use futures_util::future::{select, Either};
match select(tasks_fut, scheduler_fut).await {
// Tasks themselves don't generate work
Either::Left((_, _)) => {}
// Save these messages in FIFO to be processed later
Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()),
2021-09-02 03:22:34 +00:00
}
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-10-16 21:37:28 +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-11-07 03:11:17 +00:00
pub fn work_with_deadline<'a>(
&'a mut self,
mut deadline: impl FnMut() -> bool,
) -> Vec<Mutations<'a>> {
/*
Strategy:
- When called, check for any UI events that might've been received since the last frame.
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
2021-06-15 14:02:46 +00:00
2021-11-07 03:11:17 +00:00
- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high,"
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
'static components have a leg up here since their work can be re-used among multiple scopes.
"High priority" is only for blocking! Should only be used on "clicks"
2021-08-24 20:29:10 +00:00
2021-11-07 03:11:17 +00:00
- If there are no pending discrete events, then check for continuous events. These can be completely batched
2021-10-04 05:28:04 +00:00
2021-11-07 03:11:17 +00:00
- we batch completely until we run into a discrete event
- all continuous events are batched together
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
- we need to keep track of element parents to be able to traverse properly
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
Open questions:
- what if we get two clicks from the component during the same slice?
- should we batch?
- react says no - they are continuous
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
*/
let mut committed_mutations = Vec::<Mutations<'static>>::new();
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
while self.has_any_work() {
while let Ok(Some(msg)) = self.receiver.try_next() {
match msg {
SchedulerMsg::Immediate(im) => {
self.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(evt) => {
self.ui_events.push_back(evt);
}
}
}
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
// switch our priority, pop off any work
while let Some(event) = self.ui_events.pop_front() {
if let Some(scope) = self.get_scope_mut(&event.scope) {
if let Some(element) = event.mounted_dom_id {
log::info!("Calling listener {:?}, {:?}", event.scope, element);
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
// TODO: bubble properly here
scope.call_listener(event, element);
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
match dirty_scope {
SchedulerMsg::Immediate(im) => {
self.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e),
}
}
}
}
}
2021-11-07 03:11:17 +00:00
let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations);
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
if !work_complete {
return committed_mutations;
}
}
committed_mutations
}
}
pub enum SchedulerMsg {
// events from the host
UiEvent(UserEvent),
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
// setstate
Immediate(ScopeId),
}
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
#[derive(Debug)]
pub struct UserEvent {
/// The originator of the event trigger
pub scope: ScopeId,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// The optional real node associated with the trigger
pub mounted_dom_id: Option<ElementId>,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// The event type IE "onclick" or "onmouseover"
///
/// The name that the renderer will use to mount the listener.
pub name: &'static str,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// The type of event
pub event: Box<dyn Any + Send>,
}
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// Priority of Event Triggers.
///
/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus
/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
///
/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
/// we keep it simple, and just use a 3-tier priority system.
///
/// - NoPriority = 0
/// - LowPriority = 1
/// - NormalPriority = 2
/// - UserBlocking = 3
/// - HighPriority = 4
/// - ImmediatePriority = 5
///
/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// Work that must be completed during the EventHandler phase.
///
/// Currently this is reserved for controlled inputs.
Immediate = 3,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
///
/// This is typically reserved for things like user interaction.
///
/// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
High = 2,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
/// than "High Priority" events and will take precedence over low priority events.
///
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
///
/// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
Medium = 1,
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
/// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be
/// advanced to the front of the work queue until completed.
///
/// The primary user of Low Priority work is the asynchronous work system (Suspense).
///
/// This is considered "idle" work or "background" work.
Low = 0,
}
2021-11-05 20:28:08 +00:00
/// The scheduler holds basically everything around "working"
///
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
///
/// In Dioxus, the scheduler provides 4 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
/// out.
///
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority
/// task shift to a higher priority task that needs mutable access to the same scopes.
///
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
///
/// There's a lot of raw pointers here...
///
/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable
/// The best way to do that is a bump allocator.
///
///
///
impl VirtualDom {
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
///
/// Returns true if the lane is finished before the deadline could be met.
pub fn work_on_current_lane(
&mut self,
deadline_reached: impl FnMut() -> bool,
mutations: &mut Vec<Mutations>,
) -> bool {
// Work through the current subtree, and commit the results when it finishes
// When the deadline expires, give back the work
let saved_state = unsafe { self.load_work() };
// We have to split away some parts of ourself - current lane is borrowed mutably
2021-11-05 22:02:44 +00:00
let mut machine = unsafe { saved_state.promote() };
2021-11-05 20:28:08 +00:00
let mut ran_scopes = FxHashSet::default();
if machine.stack.is_empty() {
todo!("order scopes");
// self.dirty_scopes.retain(|id| self.get_scope(id).is_some());
// self.dirty_scopes.sort_by(|a, b| {
// let h1 = self.get_scope(a).unwrap().height;
// let h2 = self.get_scope(b).unwrap().height;
// h1.cmp(&h2).reverse()
// });
2021-11-05 20:28:08 +00:00
if let Some(scopeid) = self.dirty_scopes.pop() {
log::info!("handling dirty scope {:?}", scopeid);
if !ran_scopes.contains(&scopeid) {
ran_scopes.insert(scopeid);
log::debug!("about to run scope {:?}", scopeid);
2021-11-05 22:02:44 +00:00
// if let Some(component) = self.get_scope_mut(&scopeid) {
if self.run_scope(&scopeid) {
todo!("diff the scope")
2021-11-05 22:02:44 +00:00
// let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
// // let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
// machine.stack.scope_stack.push(scopeid);
// machine.stack.push(DiffInstruction::Diff { new, old });
2021-11-05 20:28:08 +00:00
}
2021-11-05 22:02:44 +00:00
// }
}
2021-11-05 20:28:08 +00:00
}
}
let work_completed: bool = todo!();
// let work_completed = machine.work(deadline_reached);
2021-11-05 20:28:08 +00:00
// log::debug!("raw edits {:?}", machine.mutations.edits);
let mut machine: DiffState<'static> = unsafe { std::mem::transmute(machine) };
2021-11-05 20:28:08 +00:00
// let mut saved = machine.save();
if work_completed {
for node in machine.seen_scopes.drain() {
// self.dirty_scopes.clear();
// self.ui_events.clear();
self.dirty_scopes.remove(&node);
// self.dirty_scopes.remove(&node);
}
let mut new_mutations = Mutations::new();
for edit in machine.mutations.edits.drain(..) {
new_mutations.edits.push(edit);
}
// for edit in saved.edits.drain(..) {
// new_mutations.edits.push(edit);
// }
// std::mem::swap(&mut new_mutations, &mut saved.mutations);
mutations.push(new_mutations);
// log::debug!("saved edits {:?}", mutations);
todo!();
// let mut saved = machine.save();
// self.save_work(saved);
2021-11-05 20:28:08 +00:00
true
// self.save_work(saved);
// false
} else {
false
}
}
2021-11-07 03:11:17 +00:00
/// Compute a manual diff of the VirtualDOM between states.
2021-11-05 20:28:08 +00:00
///
2021-11-07 03:11:17 +00:00
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propagated as an event.
2021-11-05 20:28:08 +00:00
///
2021-11-07 03:11:17 +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.
2021-11-05 20:28:08 +00:00
///
///
2021-11-07 03:11:17 +00:00
/// # Example
/// ```rust
/// #[derive(PartialEq, Props)]
/// struct AppProps {
/// value: Shared<&'static str>,
/// }
///
/// static App: FC<AppProps> = |(cx, props)|{
/// 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();
/// ```
pub fn hard_diff<'a>(&'a mut self, base_scope: &ScopeId) -> Mutations<'a> {
// // TODO: drain any in-flight work
// // We run the component. If it succeeds, then we can diff it and add the changes to the dom.
// if self.run_scope(&base_scope) {
// let cur_component = self
// .get_scope_mut(&base_scope)
// .expect("The base scope should never be moved");
// log::debug!("rebuild {:?}", base_scope);
// let mut diff_machine = DiffState::new(Mutations::new());
// diff_machine
// .stack
// .create_node(cur_component.frames.fin_head(), MountType::Append);
// diff_machine.stack.scope_stack.push(base_scope);
// todo!()
// // self.work(&mut diff_machine, || false);
// // diff_machine.work(|| false);
// } else {
// // todo: should this be a hard error?
// log::warn!(
// "Component failed to run successfully during rebuild.
// This does not result in a failed rebuild, but indicates a logic failure within your app."
// );
// }
2021-11-05 20:28:08 +00:00
2021-11-07 03:11:17 +00:00
// todo!()
// // unsafe { std::mem::transmute(diff_machine.mutations) }
2021-11-05 20:28:08 +00:00
let cur_component = self
2021-11-07 03:11:17 +00:00
.scopes
.get(&base_scope)
2021-11-05 20:28:08 +00:00
.expect("The base scope should never be moved");
log::debug!("hard diff {:?}", base_scope);
2021-11-05 22:02:44 +00:00
if self.run_scope(&base_scope) {
let mut diff_machine = DiffState::new(Mutations::new());
2021-11-05 20:28:08 +00:00
diff_machine.cfg.force_diff = true;
self.diff_scope(&mut diff_machine, base_scope);
// diff_machine.diff_scope(base_scope);
2021-11-05 20:28:08 +00:00
diff_machine.mutations
} else {
Mutations::new()
}
}
2021-11-05 22:02:44 +00:00
pub fn run_scope(&mut self, id: &ScopeId) -> bool {
let scope = self
2021-11-07 03:11:17 +00:00
.scopes
.get(id)
2021-11-05 22:02:44 +00:00
.expect("The base scope should never be moved");
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
scope.ensure_drop_safety();
// Safety:
// - We dropped the listeners, so no more &mut T can be used while these are held
// - All children nodes that rely on &mut T are replaced with a new reference
unsafe { scope.hooks.reset() };
// Safety:
// - We've dropped all references to the wip bump frame
todo!("reset wip frame");
// unsafe { scope.frames.reset_wip_frame() };
2021-11-05 22:02:44 +00:00
let items = scope.items.get_mut();
// just forget about our suspended nodes while we're at it
items.suspended_nodes.clear();
// guarantee that we haven't screwed up - there should be no latent references anywhere
debug_assert!(items.listeners.is_empty());
debug_assert!(items.suspended_nodes.is_empty());
debug_assert!(items.borrowed_props.is_empty());
2021-11-05 20:28:08 +00:00
2021-11-05 22:02:44 +00:00
log::debug!("Borrowed stuff is successfully cleared");
// temporarily cast the vcomponent to the right lifetime
let vcomp = scope.load_vcomp();
2021-11-07 03:11:17 +00:00
let render: &dyn Fn(&ScopeState) -> Element = todo!();
2021-11-05 22:02:44 +00:00
// Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
if let Some(builder) = render(scope) {
2021-11-07 01:07:01 +00:00
todo!("attach the niode");
// let new_head = builder.into_vnode(NodeFactory {
// bump: &scope.frames.wip_frame().bump,
// });
2021-11-05 22:02:44 +00:00
log::debug!("Render is successful");
// the user's component succeeded. We can safely cycle to the next frame
2021-11-07 01:07:01 +00:00
// scope.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
// scope.frames.cycle_frame();
2021-11-05 22:02:44 +00:00
true
} else {
false
}
2021-09-13 23:07:15 +00:00
}
pub fn reserve_node(&self, node: &VNode) -> ElementId {
todo!()
// self.node_reservations.insert(id);
}
pub fn collect_garbage(&self, id: ElementId) {
todo!()
}
2021-11-07 03:11:17 +00:00
pub fn try_remove(&self, id: &ScopeId) -> Option<ScopeState> {
todo!()
}
2021-09-13 23:07:15 +00:00
}