diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index a6e22108d..f25c6ae47 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -23,13 +23,8 @@ fxhash = "0.2" # Used in diffing longest-increasing-subsequence = "0.1.0" -# internall used -log = { version = "0.4" } - futures-util = { version = "0.3", default-features = false } -smallvec = "1.6" - slab = "0.4" futures-channel = "0.3.21" @@ -38,7 +33,6 @@ indexmap = "1.7" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } -futures-task = "0.3.25" [dev-dependencies] tokio = { version = "*", features = ["full"] } diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index eb9ac1598..c017ae8fc 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -21,7 +21,7 @@ where { pub render_fn: fn(Scope<'a, P>) -> F, pub memo: unsafe fn(&P, &P) -> bool, - pub props: *const P, + pub props: P, pub _marker: PhantomData, } @@ -30,7 +30,7 @@ impl<'a> VComponentProps<'a, (), ()> { Self { render_fn, memo: <() as PartialEq>::eq, - props: std::ptr::null_mut(), + props: (), _marker: PhantomData, } } @@ -40,7 +40,7 @@ impl<'a, P, A, F: ComponentReturn<'a, A>> VComponentProps<'a, P, A, F> { pub(crate) fn new( render_fn: fn(Scope<'a, P>) -> F, memo: unsafe fn(&P, &P) -> bool, - props: *const P, + props: P, ) -> Self { Self { render_fn, @@ -66,12 +66,9 @@ impl<'a, P, A, F: ComponentReturn<'a, A>> AnyProps<'a> for VComponentProps<'a, P (self.memo)(real_us, real_other) } - fn render(&self, cx: &'a ScopeState) -> RenderReturn<'a> { - // Make sure the scope ptr is not null - // self.props.state.set(scope); - + fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { let scope = cx.bump().alloc(Scoped { - props: unsafe { &*self.props }, + props: &self.props, scope: cx, }); diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index ff6f3229e..30c9881cb 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,20 +1,32 @@ -use std::pin::Pin; - -use crate::factory::{FiberLeaf, RenderReturn}; -use crate::innerlude::{Renderer, SuspenseContext}; +use crate::factory::RenderReturn; +use crate::innerlude::{Mutations, SuspenseContext}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, Element, ElementId, TemplateAttribute}; -use bumpalo::boxed::Box as BumpBox; -use futures_util::Future; +use crate::{AttributeValue, ElementId, ScopeId, TemplateAttribute}; impl VirtualDom { + pub(crate) fn create_scope<'a>( + &mut self, + scope: ScopeId, + mutations: &mut Mutations<'a>, + template: &'a VNode<'a>, + ) -> usize { + self.scope_stack.push(scope); + let out = self.create(mutations, template); + self.scope_stack.pop(); + out + } + /// Create this template and write its mutations - pub fn create<'a>(&mut self, mutations: &mut Renderer<'a>, template: &'a VNode<'a>) -> usize { - // The best renderers will have templates prehydrated + pub(crate) fn create<'a>( + &mut self, + mutations: &mut Mutations<'a>, + template: &'a VNode<'a>, + ) -> usize { + // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways if !self.templates.contains_key(&template.template.id) { for node in template.template.roots { @@ -31,7 +43,7 @@ impl VirtualDom { .insert(template.template.id, template.template.clone()); } - // Walk the roots backwards, creating nodes and assigning IDs + // Walk the roots, creating nodes and assigning IDs // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS) let mut dynamic_attrs = template.template.attr_paths.iter().enumerate().peekable(); let mut dynamic_nodes = template.template.node_paths.iter().enumerate().peekable(); @@ -39,25 +51,23 @@ impl VirtualDom { let mut on_stack = 0; for (root_idx, root) in template.template.roots.iter().enumerate() { on_stack += match root { - TemplateNode::Element { .. } | TemplateNode::Text(_) => { + TemplateNode::Element { .. } + | TemplateNode::Text(_) + | TemplateNode::DynamicText { .. } => { mutations.push(LoadTemplate { name: template.template.id, index: root_idx, }); - 1 } TemplateNode::Dynamic(id) => { self.create_dynamic_node(mutations, template, &template.dynamic_nodes[*id], *id) } - - TemplateNode::DynamicText { .. } => 1, }; // we're on top of a node that has a dynamic attribute for a descendant // Set that attribute now before the stack gets in a weird state - while let Some((mut attr_id, path)) = dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8) { @@ -67,18 +77,17 @@ impl VirtualDom { id, }); - // set any future attrs with the same path (ie same element) loop { - let attr = &template.dynamic_attrs[attr_id]; - attr.mounted_element.set(id); + let attribute = &template.dynamic_attrs[attr_id]; + attribute.mounted_element.set(id); - match attr.value { + match attribute.value { AttributeValue::Text(value) => mutations.push(SetAttribute { - name: attr.name, + name: attribute.name, value, id, }), - AttributeValue::Listener(_) => {} + AttributeValue::Listener(_) => todo!("create listener attributes"), AttributeValue::Float(_) => todo!(), AttributeValue::Int(_) => todo!(), AttributeValue::Bool(_) => todo!(), @@ -86,10 +95,10 @@ impl VirtualDom { AttributeValue::None => todo!(), } - if let Some((next_attr_id, _)) = dynamic_attrs.next_if(|(_, p)| *p == path) { - attr_id = next_attr_id - } else { - break; + // Only push the dynamic attributes forward if they match the current path (same element) + match dynamic_attrs.next_if(|(_, p)| *p == path) { + Some((next_attr_id, _)) => attr_id = next_attr_id, + None => break, } } } @@ -113,7 +122,7 @@ impl VirtualDom { on_stack } - pub fn create_static_node<'a>( + pub(crate) fn create_static_node<'a>( &mut self, mutations: &mut Vec>, template: &'a VNode<'a>, @@ -156,9 +165,9 @@ impl VirtualDom { } } - pub fn create_dynamic_node<'a>( + pub(crate) fn create_dynamic_node<'a>( &mut self, - mutations: &mut Renderer<'a>, + mutations: &mut Mutations<'a>, template: &'a VNode<'a>, node: &'a DynamicNode<'a>, idx: usize, @@ -172,24 +181,22 @@ impl VirtualDom { path: &template.template.node_paths[idx][1..], value, }); - 1 } DynamicNode::Component { props, placeholder, .. } => { - let id = self.new_scope(unsafe { std::mem::transmute(props.get()) }); - let render_ret = self.run_scope(id); - let render_ret: &mut RenderReturn = unsafe { std::mem::transmute(render_ret) }; + let scope = self + .new_scope(unsafe { std::mem::transmute(props.get()) }) + .id; + let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; - // if boundary or subtree, start working on a new stack of mutations - - match render_ret { + match return_nodes { RenderReturn::Sync(None) | RenderReturn::Async(_) => { let new_id = self.next_element(template); placeholder.set(Some(new_id)); - self.scopes[id.0].placeholder.set(Some(new_id)); + self.scopes[scope.0].placeholder.set(Some(new_id)); mutations.push(AssignId { id: new_id, path: &template.template.node_paths[idx][1..], @@ -200,28 +207,28 @@ impl VirtualDom { RenderReturn::Sync(Some(template)) => { let mutations_to_this_point = mutations.len(); - self.scope_stack.push(id); + self.scope_stack.push(scope); let mut created = self.create(mutations, template); self.scope_stack.pop(); - if !self.waiting_on.is_empty() { + if !self.collected_leaves.is_empty() { if let Some(boundary) = - self.scopes[id.0].has_context::() + self.scopes[scope.0].has_context::() { let mut boundary_mut = boundary.borrow_mut(); let split_off = mutations.split_off(mutations_to_this_point); let split_off = unsafe { std::mem::transmute(split_off) }; - println!("SPLIT OFF: {:#?}", split_off); - boundary_mut.mutations.mutations = split_off; - boundary_mut.waiting_on.extend(self.waiting_on.drain(..)); + boundary_mut + .waiting_on + .extend(self.collected_leaves.drain(..)); // Since this is a boundary, use it as a placeholder let new_id = self.next_element(template); placeholder.set(Some(new_id)); - self.scopes[id.0].placeholder.set(Some(new_id)); + self.scopes[scope.0].placeholder.set(Some(new_id)); mutations.push(AssignId { id: new_id, path: &template.template.node_paths[idx][1..], diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index b34155b7a..f5a51b6ba 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,6 +1,6 @@ use std::any::Any; -use crate::innerlude::Renderer; +use crate::innerlude::Mutations; use crate::virtual_dom::VirtualDom; use crate::{Attribute, AttributeValue, TemplateNode}; @@ -20,19 +20,32 @@ use crate::{ use fxhash::{FxHashMap, FxHashSet}; use slab::Slab; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DirtyScope { - height: usize, - id: ScopeId, + pub height: u32, + pub id: ScopeId, +} + +impl PartialOrd for DirtyScope { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.height.cmp(&other.height)) + } +} + +impl Ord for DirtyScope { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.height.cmp(&other.height) + } } impl<'b> VirtualDom { - pub fn diff_scope(&mut self, mutations: &mut Renderer<'b>, scope: ScopeId) { + pub fn diff_scope(&mut self, mutations: &mut Mutations<'b>, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; } pub fn diff_node( &mut self, - muts: &mut Renderer<'b>, + muts: &mut Mutations<'b>, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>, ) { @@ -177,7 +190,7 @@ impl<'b> VirtualDom { // the change list stack is in the same state when this function returns. fn diff_non_keyed_children( &mut self, - muts: &mut Renderer<'b>, + muts: &mut Mutations<'b>, old: &'b [VNode<'b>], new: &'b [VNode<'b>], ) { @@ -217,7 +230,7 @@ impl<'b> VirtualDom { // The stack is empty upon entry. fn diff_keyed_children( &mut self, - muts: &mut Renderer<'b>, + muts: &mut Mutations<'b>, old: &'b [VNode<'b>], new: &'b [VNode<'b>], ) { @@ -533,7 +546,7 @@ impl<'b> VirtualDom { /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes - fn remove_nodes(&mut self, muts: &mut Renderer<'b>, nodes: &'b [VNode<'b>]) { + fn remove_nodes(&mut self, muts: &mut Mutations<'b>, nodes: &'b [VNode<'b>]) { // } } diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index a75b99e42..a6779e479 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -80,7 +80,6 @@ impl ScopeState { where P: Properties + 'a, { - let props = self.bump().alloc(props); let as_component = component; let vcomp = VComponentProps::new(as_component, P::memoize, props); let as_dyn = self.bump().alloc(vcomp) as &mut dyn AnyProps; @@ -136,6 +135,15 @@ pub enum RenderReturn<'a> { Async(BumpBox<'a, dyn Future> + 'a>), } +impl<'a> RenderReturn<'a> { + pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } + pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } +} + pub type FiberLeaf<'a> = Pin> + 'a>>; pub trait IntoVnode<'a, A = ()> { diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index f9405bef4..5f76dc5ad 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,13 +1,13 @@ use crate::arena::ElementId; #[derive(Debug)] -pub struct Renderer<'a> { +pub struct Mutations<'a> { pub subtree: usize, pub template_mutations: Vec>, pub mutations: Vec>, } -impl<'a> Renderer<'a> { +impl<'a> Mutations<'a> { pub fn new(subtree: usize) -> Self { Self { subtree, @@ -17,7 +17,7 @@ impl<'a> Renderer<'a> { } } -impl<'a> std::ops::Deref for Renderer<'a> { +impl<'a> std::ops::Deref for Mutations<'a> { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -25,52 +25,12 @@ impl<'a> std::ops::Deref for Renderer<'a> { } } -impl std::ops::DerefMut for Renderer<'_> { +impl std::ops::DerefMut for Mutations<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.mutations } } -// impl<'a> Renderer<'a> { -// pub fn new(subtree: usize) -> Self { -// Self { -// mutations: vec![Mutations { -// subtree, -// mutations: Vec::new(), -// }], -// } -// } -// } -// impl<'a> Renderer<'a> { -// pub fn push(&mut self, mutation: Mutation<'a>) { -// self.mutations.last_mut().unwrap().mutations.push(mutation) -// } - -// pub fn extend(&mut self, mutations: impl IntoIterator>) { -// self.mutations -// .last_mut() -// .unwrap() -// .mutations -// .extend(mutations) -// } - -// pub fn len(&self) -> usize { -// self.mutations.last().unwrap().mutations.len() -// } - -// pub fn split_off(&mut self, idx: usize) -> Renderer<'a> { -// let mut mutations = self.mutations.split_off(idx); -// let subtree = mutations.pop().unwrap().subtree; -// Renderer { mutations } -// } -// } - -// #[derive(Debug)] -// pub struct Mutations<'a> { -// subtree: usize, -// mutations: Vec>, -// } - /* each subtree has its own numbering scheme */ diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 3942de69d..3388dff97 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -49,7 +49,7 @@ impl<'a> VNode<'a> { } pub fn single_text( - cx: &'a ScopeState, + _cx: &'a ScopeState, text: &'static [TemplateNode<'static>], id: &'static str, ) -> Option { diff --git a/packages/core/src/scheduler/bumpslab.rs b/packages/core/src/scheduler/bumpslab.rs deleted file mode 100644 index 8b1378917..000000000 --- a/packages/core/src/scheduler/bumpslab.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/core/src/scheduler/handle.rs b/packages/core/src/scheduler/handle.rs deleted file mode 100644 index 8525ba8de..000000000 --- a/packages/core/src/scheduler/handle.rs +++ /dev/null @@ -1,35 +0,0 @@ -use futures_util::Future; -use slab::Slab; -use std::{cell::RefCell, pin::Pin, rc::Rc, sync::Arc}; - -use super::{LocalTask, SchedulerMsg, SuspenseLeaf}; - -#[derive(Clone)] -pub struct SchedulerHandle(Rc); - -impl std::ops::Deref for SchedulerHandle { - type Target = HandleInner; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct HandleInner { - pub sender: futures_channel::mpsc::UnboundedSender, - - /// Tasks created with cx.spawn - pub tasks: RefCell>>, - - /// Async components - pub leaves: RefCell>>, -} - -impl SchedulerHandle { - pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Self { - Self(Rc::new(HandleInner { - sender, - tasks: RefCell::new(Slab::new()), - leaves: RefCell::new(Slab::new()), - })) - } -} diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 6e56deec8..a1458e2cd 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,16 +1,11 @@ -use slab::Slab; -use std::sync::Arc; - use crate::ScopeId; +use slab::Slab; -mod bumpslab; -mod handle; mod suspense; mod task; mod wait; mod waker; -pub use handle::*; pub use suspense::*; pub use task::*; pub use waker::RcWake; @@ -36,26 +31,34 @@ pub enum SchedulerMsg { SuspenseNotified(SuspenseId), } -pub struct Scheduler { - rx: futures_channel::mpsc::UnboundedReceiver, - ready_suspense: Vec, - pub handle: SchedulerHandle, +use std::{cell::RefCell, rc::Rc}; + +#[derive(Clone)] +pub(crate) struct Scheduler(Rc); + +impl std::ops::Deref for Scheduler { + type Target = HandleInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct HandleInner { + pub sender: futures_channel::mpsc::UnboundedSender, + + /// Tasks created with cx.spawn + pub tasks: RefCell>>, + + /// Async components + pub leaves: RefCell>>, } impl Scheduler { - pub fn new() -> Self { - let (tx, rx) = futures_channel::mpsc::unbounded(); - Self { - rx, - handle: SchedulerHandle::new(tx), - ready_suspense: Default::default(), - } - } - - /// Waits for a future to complete that marks the virtualdom as dirty - /// - /// Not all messages will mark a virtualdom as dirty, so this waits for a message that has side-effects that do - pub fn wait_for_work(&mut self) { - // + pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Self { + Self(Rc::new(HandleInner { + sender, + tasks: RefCell::new(Slab::new()), + leaves: RefCell::new(Slab::new()), + })) } } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index f4c9ac140..62168d746 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,16 +1,15 @@ use std::{ cell::{Cell, RefCell}, collections::HashSet, - pin::Pin, rc::Rc, }; use super::{waker::RcWake, SchedulerMsg}; use crate::{ - innerlude::{Mutation, Renderer}, + innerlude::{Mutation, Mutations}, Element, ScopeId, }; -use futures_task::Waker; + use futures_util::Future; #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -22,7 +21,7 @@ pub type SuspenseContext = Rc>; pub struct SuspenseBoundary { pub id: ScopeId, pub waiting_on: HashSet, - pub mutations: Renderer<'static>, + pub mutations: Mutations<'static>, } impl SuspenseBoundary { @@ -30,7 +29,7 @@ impl SuspenseBoundary { Rc::new(RefCell::new(Self { id, waiting_on: Default::default(), - mutations: Renderer::new(0), + mutations: Mutations::new(0), })) } } @@ -46,10 +45,7 @@ pub struct SuspenseLeaf { impl RcWake for SuspenseLeaf { fn wake_by_ref(arc_self: &Rc) { - // if arc_self.notified.get() { - // return; - // } - // arc_self.notified.set(true); + arc_self.notified.set(true); _ = arc_self .tx .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index 692f9f436..4ab08669f 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -7,15 +7,16 @@ use std::{ process::Output, rc::Rc, sync::Arc, + task::Poll, }; -use futures_task::{waker, ArcWake, Context, RawWaker, RawWakerVTable, Waker}; use futures_util::{pin_mut, Future, FutureExt}; use slab::Slab; +use std::task::{Context, RawWaker, RawWakerVTable, Waker}; use crate::{Element, ScopeId}; -use super::{waker::RcWake, HandleInner, SchedulerHandle, SchedulerMsg}; +use super::{waker::RcWake, HandleInner, Scheduler, SchedulerMsg}; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -24,12 +25,29 @@ pub struct TaskId(pub usize); /// the task itself is the waker pub struct LocalTask { - id: TaskId, - scope: ScopeId, - tx: futures_channel::mpsc::UnboundedSender, + pub id: TaskId, + pub scope: ScopeId, + pub tx: futures_channel::mpsc::UnboundedSender, + + // todo: use rc and weak, or the bump slab instead of unsafecell pub task: UnsafeCell + 'static>>>, } +impl LocalTask { + pub fn progress(self: &Rc) -> bool { + let waker = self.waker(); + let mut cx = Context::from_waker(&waker); + + // safety: the waker owns its task and everythig is single threaded + let fut = unsafe { &mut *self.task.get() }; + + match fut.poll_unpin(&mut cx) { + Poll::Ready(_) => true, + _ => false, + } + } +} + impl HandleInner { pub fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { let mut tasks = self.tasks.borrow_mut(); diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 03b9a57d5..ff9afe79f 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,9 +1,10 @@ -use futures_task::Context; use futures_util::{FutureExt, StreamExt}; +use std::task::{Context, Poll}; use crate::{ + diff::DirtyScope, factory::RenderReturn, - innerlude::{Mutation, Renderer, SuspenseContext}, + innerlude::{Mutation, Mutations, SuspenseContext}, VNode, VirtualDom, }; @@ -14,25 +15,29 @@ impl VirtualDom { /// /// This is cancel safe, so if the future is dropped, you can push events into the virtualdom pub async fn wait_for_work(&mut self) { + // todo: make sure the scheduler queue is completely drained loop { - match self.scheduler.rx.next().await.unwrap() { - SchedulerMsg::Event => todo!(), - SchedulerMsg::Immediate(_) => todo!(), + match self.rx.next().await.unwrap() { + SchedulerMsg::Event => break, + + SchedulerMsg::Immediate(id) => { + let height = self.scopes[id.0].height; + self.dirty_scopes.insert(DirtyScope { height, id }); + break; + } + SchedulerMsg::DirtyAll => todo!(), SchedulerMsg::TaskNotified(id) => { - let mut tasks = self.scheduler.handle.tasks.borrow_mut(); - let local_task = &tasks[id.0]; + let mut tasks = self.scheduler.tasks.borrow_mut(); + let task = &tasks[id.0]; - // attach the waker to itself - // todo: don't make a new waker every time, make it once and then just clone it - let waker = local_task.waker(); - let mut cx = Context::from_waker(&waker); + // If the task completes... + if task.progress() { + // Remove it from the scope so we dont try to double drop it when the scope dropes + self.scopes[task.scope.0].spawned_tasks.remove(&id); - // safety: the waker owns its task and everythig is single threaded - let fut = unsafe { &mut *local_task.task.get() }; - - if let futures_task::Poll::Ready(_) = fut.poll_unpin(&mut cx) { + // Remove it from the scheduler tasks.remove(id.0); } } @@ -42,7 +47,6 @@ impl VirtualDom { let leaf = self .scheduler - .handle .leaves .borrow_mut() .get(id.0) @@ -55,16 +59,14 @@ impl VirtualDom { let waker = leaf.waker(); let mut cx = Context::from_waker(&waker); - let fut = unsafe { &mut *leaf.task }; - - let mut pinned = unsafe { std::pin::Pin::new_unchecked(fut) }; + // Safety: the future is always pinned to the bump arena + let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; let as_pinned_mut = &mut pinned; // the component finished rendering and gave us nodes // we should attach them to that component and then render its children // continue rendering the tree until we hit yet another suspended component - if let futures_task::Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) - { + if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { let boundary = &self.scopes[leaf.scope_id.0] .consume_context::() .unwrap(); @@ -87,7 +89,7 @@ impl VirtualDom { if let RenderReturn::Sync(Some(template)) = ret { let mutations = &mut fiber.mutations; let template: &VNode = unsafe { std::mem::transmute(template) }; - let mutations: &mut Renderer = + let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; self.scope_stack.push(scope_id); @@ -103,8 +105,6 @@ impl VirtualDom { } } } - - // now proces any events. If we end up running a component and it generates mutations, then we should run those mutations } } } diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs index 9add24ba4..762020167 100644 --- a/packages/core/src/scheduler/waker.rs +++ b/packages/core/src/scheduler/waker.rs @@ -1,4 +1,4 @@ -use futures_task::{RawWaker, RawWakerVTable, Waker}; +use std::task::{RawWaker, RawWakerVTable, Waker}; use std::{mem, rc::Rc}; pub trait RcWake: Sized { diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 49902cb88..251a3de66 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,5 +1,14 @@ -use crate::{innerlude::SuspenseContext, scheduler::RcWake}; -use futures_util::{pin_mut, task::noop_waker_ref}; +use crate::{ + any_props::AnyProps, + arena::ElementId, + bump_frame::BumpFrame, + factory::RenderReturn, + innerlude::{SuspenseId, SuspenseLeaf}, + scheduler::RcWake, + scopes::{ScopeId, ScopeState}, + virtual_dom::VirtualDom, +}; +use futures_util::FutureExt; use std::{ mem, pin::Pin, @@ -7,18 +16,8 @@ use std::{ task::{Context, Poll}, }; -use crate::{ - any_props::AnyProps, - arena::ElementId, - bump_frame::BumpFrame, - factory::RenderReturn, - innerlude::{SuspenseId, SuspenseLeaf}, - scopes::{ScopeId, ScopeState}, - virtual_dom::VirtualDom, -}; - impl VirtualDom { - pub fn new_scope(&mut self, props: *mut dyn AnyProps<'static>) -> ScopeId { + pub fn new_scope(&mut self, props: *mut dyn AnyProps<'static>) -> &mut ScopeState { let parent = self.acquire_current_scope_raw(); let container = self.acquire_current_container(); let entry = self.scopes.vacant_entry(); @@ -34,18 +33,17 @@ impl VirtualDom { placeholder: None.into(), node_arena_1: BumpFrame::new(50), node_arena_2: BumpFrame::new(50), + spawned_tasks: Default::default(), render_cnt: Default::default(), hook_arena: Default::default(), hook_vals: Default::default(), hook_idx: Default::default(), shared_contexts: Default::default(), - tasks: self.scheduler.handle.clone(), - }); - - id + tasks: self.scheduler.clone(), + }) } - pub fn acquire_current_container(&self) -> ElementId { + fn acquire_current_container(&self) -> ElementId { self.element_stack .last() .copied() @@ -59,39 +57,45 @@ impl VirtualDom { .and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState)) } - pub fn run_scope(&mut self, scope_id: ScopeId) -> &mut RenderReturn { + pub(crate) unsafe fn run_scope_extend<'a>( + &mut self, + scope_id: ScopeId, + ) -> &'a RenderReturn<'a> { + unsafe { self.run_scope(scope_id).extend_lifetime_ref() } + } + + pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { let mut new_nodes = unsafe { let scope = &mut self.scopes[scope_id.0]; scope.hook_idx.set(0); + // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &mut dyn AnyProps = mem::transmute(&mut *scope.props); - let res: RenderReturn = props.render(scope); - let res: RenderReturn<'static> = mem::transmute(res); - res + props.render(scope).extend_lifetime() }; // immediately resolve futures that can be resolved if let RenderReturn::Async(task) = &mut new_nodes { - use futures_util::FutureExt; + let mut leaves = self.scheduler.leaves.borrow_mut(); - let mut leaves = self.scheduler.handle.leaves.borrow_mut(); let entry = leaves.vacant_entry(); - let key = entry.key(); - let suspense_id = SuspenseId(key); + let suspense_id = SuspenseId(entry.key()); let leaf = Rc::new(SuspenseLeaf { scope_id, task: task.as_mut(), id: suspense_id, - tx: self.scheduler.handle.sender.clone(), - notified: false.into(), + tx: self.scheduler.sender.clone(), + notified: Default::default(), }); - let _leaf = leaf.clone(); let waker = leaf.waker(); let mut cx = Context::from_waker(&waker); + + // safety: the task is already pinned in the bump arena let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; + // Keep polling until either we get a value or the future is not ready loop { match pinned.poll_unpin(&mut cx) { // If nodes are produced, then set it and we can break @@ -103,8 +107,8 @@ impl VirtualDom { // If no nodes are produced but the future woke up immediately, then try polling it again // This circumvents things like yield_now, but is important is important when rendering // components that are just a stream of immediately ready futures - _ if _leaf.notified.get() => { - _leaf.notified.set(false); + _ if leaf.notified.get() => { + leaf.notified.set(false); continue; } @@ -112,7 +116,7 @@ impl VirtualDom { // Insert the future into fiber leaves and break _ => { entry.insert(leaf); - self.waiting_on.push(suspense_id); + self.collected_leaves.push(suspense_id); break; } }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index bfc3ee9bc..00b7f13f7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,7 +1,7 @@ use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, sync::Arc, }; @@ -13,7 +13,7 @@ use crate::{ any_props::AnyProps, arena::ElementId, bump_frame::BumpFrame, - innerlude::{SchedulerHandle, SchedulerMsg}, + innerlude::{Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::VNode, TaskId, @@ -43,27 +43,28 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> { pub struct ScopeId(pub usize); pub struct ScopeState { - pub render_cnt: usize, + pub(crate) render_cnt: usize, - pub node_arena_1: BumpFrame, - pub node_arena_2: BumpFrame, + pub(crate) node_arena_1: BumpFrame, + pub(crate) node_arena_2: BumpFrame, - pub parent: Option<*mut ScopeState>, - pub container: ElementId, - pub id: ScopeId, + pub(crate) parent: Option<*mut ScopeState>, + pub(crate) container: ElementId, + pub(crate) id: ScopeId, - pub height: u32, + pub(crate) height: u32, - pub hook_arena: Bump, - pub hook_vals: RefCell>, - pub hook_idx: Cell, + pub(crate) hook_arena: Bump, + pub(crate) hook_vals: RefCell>, + pub(crate) hook_idx: Cell, pub(crate) shared_contexts: RefCell>>, - pub tasks: SchedulerHandle, + pub(crate) tasks: Scheduler, + pub(crate) spawned_tasks: HashSet, - pub props: *mut dyn AnyProps<'static>, - pub placeholder: Cell>, + pub(crate) props: *mut dyn AnyProps<'static>, + pub(crate) placeholder: Cell>, } impl ScopeState { @@ -243,14 +244,11 @@ impl ScopeState { let parent = unsafe { &*parent }; if parent.scope_id() == ScopeId(0) { - let exists = parent + let _ = parent .shared_contexts .borrow_mut() .insert(TypeId::of::(), Box::new(value.clone())); - if exists.is_some() { - log::warn!("Context already provided to parent scope - replacing it"); - } return value; } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9ed2b483e..a20f46ab6 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,12 +1,10 @@ use crate::any_props::VComponentProps; use crate::arena::ElementPath; -use crate::component::Component; use crate::diff::DirtyScope; use crate::factory::RenderReturn; -use crate::innerlude::{Renderer, Scheduler, SchedulerMsg}; +use crate::innerlude::{Mutations, Scheduler, SchedulerMsg}; use crate::mutations::Mutation; use crate::nodes::{Template, TemplateId}; - use crate::{ arena::ElementId, scopes::{ScopeId, ScopeState}, @@ -14,10 +12,101 @@ use crate::{ use crate::{scheduler, Element, Scope}; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::Future; -use scheduler::{SuspenseBoundary, SuspenseContext, SuspenseId}; +use scheduler::{SuspenseBoundary, SuspenseId}; use slab::Slab; use std::collections::{BTreeSet, HashMap}; +/// A virtual node system that progresses user events and diffs UI trees. +/// +/// ## Guide +/// +/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. +/// +/// ```rust, ignore +/// #[derive(Props, PartialEq)] +/// struct AppProps { +/// title: String +/// } +/// +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!( +/// div {"hello, {cx.props.title}"} +/// )) +/// } +/// ``` +/// +/// Components may be composed to make complex apps. +/// +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!( +/// NavBar { routes: ROUTES } +/// Title { "{cx.props.title}" } +/// Footer {} +/// )) +/// } +/// ``` +/// +/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to +/// draw the UI. +/// +/// ```rust, ignore +/// let mut vdom = VirtualDom::new(App); +/// let edits = vdom.rebuild(); +/// ``` +/// +/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. +/// +/// ```rust, ignore +/// let channel = vdom.get_scheduler_channel(); +/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { +/// // ... +/// })) +/// ``` +/// +/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. +/// +/// ```rust, ignore +/// vdom.wait_for_work().await; +/// ``` +/// +/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and +/// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be +/// handled by the renderer. +/// +/// ```rust, ignore +/// let mutations = vdom.work_with_deadline(|| false); +/// for edit in mutations { +/// apply(edit); +/// } +/// ``` +/// +/// ## Building an event loop around Dioxus: +/// +/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. +/// +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// div { "Hello World" } +/// }) +/// } +/// +/// async fn main() { +/// let mut dom = VirtualDom::new(App); +/// +/// let mut inital_edits = dom.rebuild(); +/// apply_edits(inital_edits); +/// +/// loop { +/// dom.wait_for_work().await; +/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16)); +/// let deadline = || (&mut frame_timeout).now_or_never(); +/// let edits = dom.run_with_deadline(deadline).await; +/// apply_edits(edits); +/// } +/// } +/// ``` pub struct VirtualDom { pub(crate) templates: HashMap>, pub(crate) elements: Slab, @@ -28,59 +117,153 @@ pub struct VirtualDom { // While diffing we need some sort of way of breaking off a stream of suspended mutations. pub(crate) scope_stack: Vec, - pub(crate) waiting_on: Vec, + pub(crate) collected_leaves: Vec, + + // Whenever a suspense tree is finished, we push its boundary onto this stack. + // When "render_with_deadline" is called, we pop the stack and return the mutations + pub(crate) finished_fibers: Vec, + + pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, } impl VirtualDom { + /// Create a new VirtualDom with a component that does not have special props. + /// + /// # Description + /// + /// 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. + /// + /// + /// # Example + /// ```rust, ignore + /// fn Example(cx: Scope) -> Element { + /// cx.render(rsx!( div { "hello world" } )) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` + /// + /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. pub fn new(app: fn(Scope) -> Element) -> Self { - let scheduler = Scheduler::new(); + Self::new_with_props(app, ()) + } - let mut res = Self { + /// Create a new VirtualDom with the given properties for the root component. + /// + /// # Description + /// + /// 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. + /// + /// + /// # Example + /// ```rust, ignore + /// #[derive(PartialEq, Props)] + /// struct SomeProps { + /// name: &'static str + /// } + /// + /// fn Example(cx: Scope) -> Element { + /// cx.render(rsx!{ div{ "hello {cx.props.name}" } }) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` + /// + /// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it. + /// + /// ```rust, ignore + /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); + /// let mutations = dom.rebuild(); + /// ``` + pub fn new_with_props

(root: fn(Scope

) -> Element, root_props: P) -> Self + where + P: 'static, + { + let channel = futures_channel::mpsc::unbounded(); + Self::new_with_props_and_scheduler(root, root_props, channel) + } + + /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler + /// + /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the + /// VirtualDom to be created just to retrieve its channel receiver. + /// + /// ```rust, ignore + /// let channel = futures_channel::mpsc::unbounded(); + /// let dom = VirtualDom::new_with_scheduler(Example, (), channel); + /// ``` + pub fn new_with_props_and_scheduler( + root: fn(Scope

) -> Element, + root_props: P, + (tx, rx): ( + UnboundedSender, + UnboundedReceiver, + ), + ) -> Self { + let mut dom = Self { + rx, + scheduler: Scheduler::new(tx), templates: Default::default(), scopes: Slab::default(), elements: Default::default(), scope_stack: Vec::new(), element_stack: vec![ElementId(0)], dirty_scopes: BTreeSet::new(), - waiting_on: Vec::new(), - scheduler, + collected_leaves: Vec::new(), + finished_fibers: Vec::new(), }; - let props = Box::into_raw(Box::new(VComponentProps::new_empty(app))); - let props: *mut VComponentProps<(), ()> = unsafe { std::mem::transmute(props) }; + dom.new_scope(Box::into_raw(Box::new(VComponentProps::new( + root, + |_, _| unreachable!(), + root_props, + )))) + // The root component is always a suspense boundary for any async children + // This could be unexpected, so we might rethink this behavior + .provide_context(SuspenseBoundary::new(ScopeId(0))); - let root = res.new_scope(props); - - // the root component is always a suspense boundary for any async children - res.scopes[root.0].provide_context(SuspenseBoundary::new(root)); - assert_eq!(root, ScopeId(0)); - - res + dom } - /// Render the virtualdom, without processing any suspense. + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// - /// This does register futures with wakers, but does not process any of them. - pub fn rebuild<'a>(&'a mut self) -> Renderer<'a> { - let mut mutations = Renderer::new(0); - let root_node: &RenderReturn = self.run_scope(ScopeId(0)); - let root_node: &RenderReturn = unsafe { std::mem::transmute(root_node) }; + /// The diff machine expects the RealDom's stack to be the root of the application. + /// + /// 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. + /// + /// Any templates previously registered will remain. + /// + /// # Example + /// ```rust, ignore + /// static App: Component = |cx| cx.render(rsx!{ "hello world" }); + /// + /// let mut dom = VirtualDom::new(); + /// let edits = dom.rebuild(); + /// + /// apply_edits(edits); + /// ``` + pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> { + let mut mutations = Mutations::new(0); - let mut created = 0; + let root_node = unsafe { self.run_scope_extend(ScopeId(0)) }; match root_node { RenderReturn::Sync(Some(node)) => { - self.scope_stack.push(ScopeId(0)); - created = self.create(&mut mutations, node); - self.scope_stack.pop(); - } - RenderReturn::Sync(None) => { - // + let m = self.create_scope(ScopeId(0), &mut mutations, node); + mutations.push(Mutation::AppendChildren { m }); } + RenderReturn::Sync(None) => {} RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), } - mutations.push(Mutation::AppendChildren { m: created }); - mutations }