chore: more refactoring, docs

This commit is contained in:
Jonathan Kelley committed 2022-11-11 18:29:27 -08:00
1 parent f7df6a9893
commit c44bd11fe5
27 files changed
+657 -382

No files matched your search

-13
View File
@@ -1,7 +1,5 @@
use std::marker::PhantomData;
use std::future::Future;
use crate::{
factory::{ComponentReturn, RenderReturn},
innerlude::Scoped,
@@ -25,17 +23,6 @@ where
pub _marker: PhantomData<A>,
}
impl<'a> VComponentProps<'a, (), ()> {
pub fn new_empty(render_fn: fn(Scope) -> Element) -> Self {
Self {
render_fn,
memo: <() as PartialEq>::eq,
props: (),
_marker: PhantomData,
}
}
}
impl<'a, P, A, F: ComponentReturn<'a, A>> VComponentProps<'a, P, A, F> {
pub(crate) fn new(
render_fn: fn(Scope<'a, P>) -> F,
+9
View File
@@ -8,6 +8,15 @@ pub struct ElementPath {
pub element: usize,
}
impl ElementPath {
pub fn null() -> Self {
Self {
template: std::ptr::null_mut(),
element: 0,
}
}
}
impl VirtualDom {
pub fn next_element(&mut self, template: &VNode) -> ElementId {
let entry = self.elements.vacant_entry();
+4
View File
@@ -21,4 +21,8 @@ impl BumpFrame {
self.bump.reset();
self.node.set(std::ptr::null_mut());
}
pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> {
unsafe { std::mem::transmute(&*self.node.get()) }
}
}
-11
View File
@@ -1,11 +0,0 @@
// pub trait IntoComponentType<T> {
// fn into_component_type(self) -> ComponentType;
// }
use std::marker::PhantomData;
use std::future::Future;
use crate::{scopes::Scope, Element};
pub type Component<'a, T = ()> = fn(Scope<'a, T>) -> Element<'a>;
+5 -2
View File
@@ -130,7 +130,10 @@ impl VirtualDom {
) {
match *node {
// Todo: create the children's template
TemplateNode::Dynamic(_) => mutations.push(CreatePlaceholder { id: ElementId(0) }),
TemplateNode::Dynamic(_) => {
let id = self.next_element(template);
mutations.push(CreatePlaceholder { id })
}
TemplateNode::Text(value) => mutations.push(CreateText { value }),
TemplateNode::DynamicText { .. } => mutations.push(CreateText {
value: "placeholder",
@@ -220,7 +223,7 @@ impl VirtualDom {
let split_off = unsafe { std::mem::transmute(split_off) };
boundary_mut.mutations.mutations = split_off;
boundary_mut.mutations.edits = split_off;
boundary_mut
.waiting_on
.extend(self.collected_leaves.drain(..));
+57 -1
View File
@@ -1,12 +1,12 @@
use std::any::Any;
use crate::factory::RenderReturn;
use crate::innerlude::Mutations;
use crate::virtual_dom::VirtualDom;
use crate::{Attribute, AttributeValue, TemplateNode};
use crate::any_props::VComponentProps;
use crate::component::Component;
use crate::mutations::Mutation;
use crate::nodes::{DynamicNode, Template, TemplateId};
use crate::scopes::Scope;
@@ -41,6 +41,62 @@ impl Ord for DirtyScope {
impl<'b> VirtualDom {
pub fn diff_scope(&mut self, mutations: &mut Mutations<'b>, scope: ScopeId) {
let scope_state = &mut self.scopes[scope.0];
let cur_arena = scope_state.current_frame();
let prev_arena = scope_state.previous_frame();
// relax the borrow checker
let cur_arena: &BumpFrame = unsafe { std::mem::transmute(cur_arena) };
let prev_arena: &BumpFrame = unsafe { std::mem::transmute(prev_arena) };
// Make sure the nodes arent null (they've been set properly)
assert_ne!(
cur_arena.node.get(),
std::ptr::null_mut(),
"Call rebuild before diffing"
);
assert_ne!(
prev_arena.node.get(),
std::ptr::null_mut(),
"Call rebuild before diffing"
);
self.scope_stack.push(scope);
let left = unsafe { prev_arena.load_node() };
let right = unsafe { cur_arena.load_node() };
self.diff_maybe_node(mutations, left, right);
self.scope_stack.pop();
}
fn diff_maybe_node(
&mut self,
m: &mut Mutations<'b>,
left: &'b RenderReturn<'b>,
right: &'b RenderReturn<'b>,
) {
use RenderReturn::{Async, Sync};
match (left, right) {
// diff
(Sync(Some(l)), Sync(Some(r))) => self.diff_node(m, l, r),
// remove old with placeholder
(Sync(Some(l)), Sync(None)) | (Sync(Some(l)), Async(_)) => {
//
let id = self.next_element(l); // todo!
m.push(Mutation::CreatePlaceholder { id });
self.drop_template(m, l, true);
}
// remove placeholder with nodes
(Sync(None), Sync(Some(_))) => {}
(Async(_), Sync(Some(v))) => {}
// nothing...
(Async(_), Async(_))
| (Sync(None), Sync(None))
| (Sync(None), Async(_))
| (Async(_), Sync(None)) => {}
}
}
pub fn diff_node(
+1 -45
View File
@@ -98,53 +98,9 @@ pub enum EventPriority {
Low = 0,
}
impl VirtualDom {
/// Returns None if no element could be found
pub fn handle_event<T: 'static>(&mut self, event: &UiEvent<T>) -> Option<()> {
let path = self.elements.get(event.element.0)?;
let location = unsafe { &mut *path.template }
.dynamic_attrs
.iter()
.position(|attr| attr.mounted_element.get() == event.element)?;
let mut index = Some((path.template, location));
let mut listeners = Vec::<&Attribute>::new();
while let Some((raw_parent, dyn_index)) = index {
let parent = unsafe { &mut *raw_parent };
let path = parent.template.node_paths[dyn_index];
listeners.extend(
parent
.dynamic_attrs
.iter()
.enumerate()
.filter_map(|(idx, attr)| {
match is_path_ascendant(parent.template.node_paths[idx], path) {
true if attr.name == event.name => Some(attr),
_ => None,
}
}),
);
index = parent.parent;
}
for listener in listeners {
if let AttributeValue::Listener(listener) = &listener.value {
(listener.borrow_mut())(&event.event)
}
}
Some(())
}
}
// ensures a strict descendant relationship
// returns false if the paths are equal
fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
pub fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
if small.len() >= big.len() {
return false;
}
+7 -10
View File
@@ -1,4 +1,4 @@
use std::{cell::Cell, fmt::Arguments, pin::Pin};
use std::{cell::Cell, fmt::Arguments};
use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
@@ -123,13 +123,6 @@ where
}
}
#[test]
fn takes_it() {
fn demo(cx: Scope) -> Element {
todo!()
}
}
pub enum RenderReturn<'a> {
Sync(Element<'a>),
Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
@@ -144,8 +137,6 @@ impl<'a> RenderReturn<'a> {
}
}
pub type FiberLeaf<'a> = Pin<BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>>;
pub trait IntoVnode<'a, A = ()> {
fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a>;
}
@@ -162,6 +153,12 @@ impl<'a, 'b> IntoVnode<'a> for VNode<'a> {
}
}
impl<'a, 'b> IntoVnode<'a> for Option<VNode<'a>> {
fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> {
self.expect("please allow optional nodes in")
}
}
impl<'a, 'b> IntoVnode<'a> for &'a VNode<'a> {
fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> {
VNode {
+13 -7
View File
@@ -1,16 +1,22 @@
use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode};
use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations};
impl VirtualDom {
impl<'b> VirtualDom {
pub fn drop_scope(&mut self, id: ScopeId) {
let scope = self.scopes.get(id.0).unwrap();
// let scope = self.scopes.get(id.0).unwrap();
let root = scope.root_node();
let root = unsafe { std::mem::transmute(root) };
// let root = scope.root_node();
// let root = unsafe { std::mem::transmute(root) };
self.drop_template(root);
// self.drop_template(root, false);
todo!()
}
pub fn drop_template<'a>(&'a mut self, template: &'a VNode<'a>) {
pub fn drop_template(
&mut self,
mutations: &mut Mutations,
template: &'b VNode<'b>,
gen_roots: bool,
) {
for node in template.dynamic_nodes.iter() {
match node {
DynamicNode::Text { id, .. } => {}
+6 -25
View File
@@ -1,7 +1,6 @@
mod any_props;
mod arena;
mod bump_frame;
mod component;
mod create;
mod diff;
mod events;
@@ -77,7 +76,10 @@ pub use crate::innerlude::{
ElementId,
ElementPath,
EventPriority,
Fragment,
LazyNodes,
Mutation,
Mutations,
NodeFactory,
Properties,
Scope,
@@ -100,9 +102,9 @@ pub use crate::innerlude::{
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::innerlude::{
fc_to_builder, Attribute, DynamicNode, Element, EventPriority, LazyNodes, NodeFactory,
Properties, Scope, ScopeId, ScopeState, Scoped, SuspenseBoundary, SuspenseContext, TaskId,
Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom,
fc_to_builder, Element, EventPriority, Fragment, LazyNodes, NodeFactory, Properties, Scope,
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent,
VNode, VirtualDom,
};
}
@@ -135,24 +137,3 @@ macro_rules! to_owned {
let mut $es = $es.to_owned();
)*}
}
/// A helper macro for values into callbacks for async environements.
///
///
macro_rules! callback {
() => {};
}
/// Convert a hook into a hook with an implicit dependency list by analyzing the closure.
///
/// ```
/// // Convert hooks with annoying dependencies into...
///
/// let val = use_effect(cx, (val,) |(val,)| println!("thing {val}"))
///
/// // a simple closure
/// let val = use_effect!(cx, |val| async { println!("thing {val}")) });
/// ```
macro_rules! make_dep_fn {
() => {};
}
+5 -5
View File
@@ -4,14 +4,14 @@ use crate::arena::ElementId;
pub struct Mutations<'a> {
pub subtree: usize,
pub template_mutations: Vec<Mutation<'a>>,
pub mutations: Vec<Mutation<'a>>,
pub edits: Vec<Mutation<'a>>,
}
impl<'a> Mutations<'a> {
pub fn new(subtree: usize) -> Self {
Self {
subtree,
mutations: Vec::new(),
edits: Vec::new(),
template_mutations: Vec::new(),
}
}
@@ -21,13 +21,13 @@ impl<'a> std::ops::Deref for Mutations<'a> {
type Target = Vec<Mutation<'a>>;
fn deref(&self) -> &Self::Target {
&self.mutations
&self.edits
}
}
impl std::ops::DerefMut for Mutations<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.mutations
&mut self.edits
}
}
@@ -35,7 +35,7 @@ impl std::ops::DerefMut for Mutations<'_> {
each subtree has its own numbering scheme
*/
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum Mutation<'a> {
SetAttribute {
name: &'a str,
+31 -30
View File
@@ -64,36 +64,37 @@ impl<'a> Properties for FragmentProps<'a> {
}
}
// /// Create inline fragments using Component syntax.
// ///
// /// ## Details
// ///
// /// Fragments capture a series of children without rendering extra nodes.
// ///
// /// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
// /// a key is needed to identify each item.
// ///
// /// ## Example
// ///
// /// ```rust, ignore
// /// rsx!{
// /// Fragment { key: "abc" }
// /// }
// /// ```
// ///
// /// ## Usage
// ///
// /// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
// /// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
// ///
// /// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
// ///
// /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
// #[allow(non_upper_case_globals, non_snake_case)]
// pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
// let i = cx.props.0.as_ref().map(|f| f.decouple());
// cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
// }
/// Create inline fragments using Component syntax.
///
/// ## Details
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
/// a key is needed to identify each item.
///
/// ## Example
///
/// ```rust, ignore
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
///
/// ## Usage
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
// let i = cx.props.0.as_ref().map(|f| f.decouple());
// cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
todo!("Fragment")
}
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
+1 -7
View File
@@ -14,16 +14,10 @@ pub use waker::RcWake;
///
/// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)]
pub enum SchedulerMsg {
/// Events from the Renderer
Event,
pub(crate) enum SchedulerMsg {
/// Immediate updates from Components that mark them as dirty
Immediate(ScopeId),
/// Mark all components as dirty and update them
DirtyAll,
/// A task has woken and needs to be progressed
TaskNotified(TaskId),
+1 -2
View File
@@ -29,12 +29,11 @@ impl SuspenseBoundary {
}
}
pub struct SuspenseLeaf {
pub(crate) struct SuspenseLeaf {
pub id: SuspenseId,
pub scope_id: ScopeId,
pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub notified: Cell<bool>,
pub task: *mut dyn Future<Output = Element<'static>>,
}
-5
View File
@@ -57,11 +57,6 @@ impl Scheduler {
pub fn remove(&self, id: TaskId) {
//
}
// Aborts the future
pub fn abort(&self, id: TaskId) {
//
}
}
impl RcWake for LocalTask {
+62 -80
View File
@@ -1,110 +1,92 @@
use futures_util::{FutureExt, StreamExt};
use futures_util::FutureExt;
use std::task::{Context, Poll};
use crate::{
diff::DirtyScope,
factory::RenderReturn,
innerlude::{Mutation, Mutations, SuspenseContext},
VNode, VirtualDom,
ScopeId, TaskId, VNode, VirtualDom,
};
use super::{waker::RcWake, SchedulerMsg, SuspenseLeaf};
use super::{waker::RcWake, SuspenseId, SuspenseLeaf};
impl VirtualDom {
/// Wait for futures internal to the virtualdom
/// Handle notifications by tasks inside the scheduler
///
/// 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.rx.next().await.unwrap() {
SchedulerMsg::Event => break,
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
/// queue
pub fn handle_task_wakeup(&mut self, id: TaskId) {
let mut tasks = self.scheduler.tasks.borrow_mut();
let task = &tasks[id.0];
SchedulerMsg::Immediate(id) => {
let height = self.scopes[id.0].height;
self.dirty_scopes.insert(DirtyScope { height, id });
break;
}
// 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);
SchedulerMsg::DirtyAll => todo!(),
// Remove it from the scheduler
tasks.remove(id.0);
}
}
SchedulerMsg::TaskNotified(id) => {
let mut tasks = self.scheduler.tasks.borrow_mut();
let task = &tasks[id.0];
pub fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
println!("suspense notified");
// 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);
let leaf = self
.scheduler
.leaves
.borrow_mut()
.get(id.0)
.unwrap()
.clone();
// Remove it from the scheduler
tasks.remove(id.0);
}
}
let scope_id = leaf.scope_id;
SchedulerMsg::SuspenseNotified(id) => {
println!("suspense notified");
// todo: cache the waker
let waker = leaf.waker();
let mut cx = Context::from_waker(&waker);
let leaf = self
.scheduler
.leaves
.borrow_mut()
.get(id.0)
.unwrap()
.clone();
// 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;
let scope_id = leaf.scope_id;
// 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 Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
let boundary = &self.scopes[leaf.scope_id.0]
.consume_context::<SuspenseContext>()
.unwrap();
// todo: cache the waker
let waker = leaf.waker();
let mut cx = Context::from_waker(&waker);
println!("ready pool");
// 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;
let mut fiber = boundary.borrow_mut();
// 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 Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
let boundary = &self.scopes[leaf.scope_id.0]
.consume_context::<SuspenseContext>()
.unwrap();
println!(
"Existing mutations {:?}, scope {:?}",
fiber.mutations, fiber.id
);
println!("ready pool");
let scope = &mut self.scopes[scope_id.0];
let arena = scope.current_frame();
let mut fiber = boundary.borrow_mut();
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
arena.node.set(ret);
println!(
"Existing mutations {:?}, scope {:?}",
fiber.mutations, fiber.id
);
if let RenderReturn::Sync(Some(template)) = ret {
let mutations = &mut fiber.mutations;
let template: &VNode = unsafe { std::mem::transmute(template) };
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
let scope = &mut self.scopes[scope_id.0];
let arena = scope.current_arena();
self.scope_stack.push(scope_id);
self.create(mutations, template);
self.scope_stack.pop();
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
arena.node.set(ret);
if let RenderReturn::Sync(Some(template)) = ret {
let mutations = &mut fiber.mutations;
let template: &VNode = unsafe { std::mem::transmute(template) };
let mutations: &mut Mutations =
unsafe { std::mem::transmute(mutations) };
self.scope_stack.push(scope_id);
self.create(mutations, template);
self.scope_stack.pop();
println!("{:#?}", mutations);
} else {
println!("nodes arent right");
}
} else {
println!("not ready");
}
}
println!("{:#?}", mutations);
} else {
println!("nodes arent right");
}
} else {
println!("not ready");
}
}
}
+10 -9
View File
@@ -7,7 +7,6 @@ use std::{
};
use bumpalo::Bump;
use futures_channel::mpsc::UnboundedSender;
use std::future::Future;
use crate::{
@@ -69,20 +68,27 @@ pub struct ScopeState {
}
impl ScopeState {
pub fn current_arena(&self) -> &BumpFrame {
pub fn current_frame(&self) -> &BumpFrame {
match self.render_cnt % 2 {
0 => &self.node_arena_1,
1 => &self.node_arena_2,
_ => unreachable!(),
}
}
pub fn previous_frame(&self) -> &BumpFrame {
match self.render_cnt % 2 {
1 => &self.node_arena_1,
0 => &self.node_arena_2,
_ => unreachable!(),
}
}
pub fn bump(&self) -> &Bump {
&self.current_arena().bump
&self.current_frame().bump
}
pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
let r = unsafe { &*self.current_arena().node.get() };
let r = unsafe { &*self.current_frame().node.get() };
unsafe { std::mem::transmute(r) }
}
@@ -142,11 +148,6 @@ impl ScopeState {
self.id
}
/// Get a handle to the raw update scheduler channel
pub fn scheduler_channel(&self) -> UnboundedSender<SchedulerMsg> {
self.tasks.sender.clone()
}
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`]
+248 -67
View File
@@ -1,17 +1,17 @@
use crate::any_props::VComponentProps;
use crate::arena::ElementPath;
use crate::diff::DirtyScope;
use crate::factory::RenderReturn;
use crate::innerlude::{Mutations, Scheduler, SchedulerMsg};
use crate::mutations::Mutation;
use crate::nodes::{Template, TemplateId};
use crate::{
any_props::VComponentProps,
arena::ElementId,
arena::ElementPath,
diff::DirtyScope,
factory::RenderReturn,
innerlude::{is_path_ascendant, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::{Template, TemplateId},
scheduler::{SuspenseBoundary, SuspenseId},
scopes::{ScopeId, ScopeState},
Attribute, AttributeValue, Element, Scope, SuspenseContext, UiEvent,
};
use crate::{scheduler, Element, Scope};
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use scheduler::{SuspenseBoundary, SuspenseId};
use futures_util::{pin_mut, FutureExt, StreamExt};
use slab::Slab;
use std::collections::{BTreeSet, HashMap};
use std::future::Future;
@@ -56,56 +56,88 @@ use std::rc::Rc;
/// let edits = vdom.rebuild();
/// ```
///
/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler.
/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
///
/// ```rust, ignore
/// let channel = vdom.get_scheduler_channel();
/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent {
/// // ...
/// }))
/// vdom.handle_event(event);
/// ```
///
/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
/// While no events are ready, 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
/// Once work is ready, call [`VirtualDom::render_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);
/// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100)));
///
/// for edit in mutations.edits {
/// real_dom.apply(edit);
/// }
/// ```
///
/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately
/// ready future to [`VirtualDom::render_with_deadline`].
///
///
/// ## 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 {
/// ```rust
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// div { "Hello World" }
/// })
/// }
///
/// async fn main() {
/// let mut dom = VirtualDom::new(App);
/// let dom = VirtualDom::new(app);
///
/// let mut inital_edits = dom.rebuild();
/// apply_edits(inital_edits);
/// real_dom.apply(dom.rebuild());
///
/// 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);
/// loop {
/// select! {
/// _ = dom.wait_for_work() => {}
/// evt = real_dom.wait_for_event() => dom.handle_event(evt),
/// }
///
/// real_dom.apply(dom.render_immediate());
/// }
/// ```
///
/// ## Waiting for suspense
///
/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases
/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
/// [`VirtualDom::render_with_deadline`] method:
///
/// ```rust
/// let dom = VirtualDom::new(app);
///
/// let deadline = tokio::time::sleep(Duration::from_millis(100));
/// let edits = dom.render_with_deadline(deadline).await;
/// ```
///
/// ## Use with streaming
///
/// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we
/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until
/// no suspended work is left.
///
/// ```
/// let dom = VirtualDom::new(app);
///
/// let deadline = tokio::time::sleep(Duration::from_millis(20));
/// let edits = dom.render_with_deadline(deadline).await;
///
/// real_dom.apply(edits);
///
/// while dom.has_suspended_work() {
/// dom.wait_for_work().await;
/// real_dom.apply(dom.render_immediate());
/// }
/// ```
pub struct VirtualDom {
@@ -186,27 +218,7 @@ impl VirtualDom {
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<P: 'static>(
root: fn(Scope<P>) -> Element,
root_props: P,
(tx, rx): (
UnboundedSender<SchedulerMsg>,
UnboundedReceiver<SchedulerMsg>,
),
) -> Self {
let (tx, rx) = futures_channel::mpsc::unbounded();
let mut dom = Self {
rx,
scheduler: Scheduler::new(tx),
@@ -220,18 +232,138 @@ impl VirtualDom {
finished_fibers: Vec::new(),
};
dom.new_scope(Box::into_raw(Box::new(VComponentProps::new(
let root = 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)));
root.provide_context(SuspenseBoundary::new(ScopeId(0)));
// the root element is always given element 0
dom.elements.insert(ElementPath::null());
dom
}
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id.0)
}
pub fn base_scope(&self) -> &ScopeState {
self.scopes.get(0).unwrap()
}
fn mark_dirty_scope(&mut self, id: ScopeId) {
let height = self.scopes[id.0].height;
self.dirty_scopes.insert(DirtyScope { height, id });
}
fn is_scope_suspended(&self, id: ScopeId) -> bool {
!self.scopes[id.0]
.consume_context::<SuspenseContext>()
.unwrap()
.borrow()
.waiting_on
.is_empty()
}
/// Returns true if there is any suspended work left to be done.
pub fn has_suspended_work(&self) -> bool {
!self.scheduler.leaves.borrow().is_empty()
}
/// Returns None if no element could be found
pub fn handle_event<T: 'static>(&mut self, event: &UiEvent<T>) -> Option<()> {
let path = self.elements.get(event.element.0)?;
let location = unsafe { &mut *path.template }
.dynamic_attrs
.iter()
.position(|attr| attr.mounted_element.get() == event.element)?;
let mut index = Some((path.template, location));
let mut listeners = Vec::<&Attribute>::new();
while let Some((raw_parent, dyn_index)) = index {
let parent = unsafe { &mut *raw_parent };
let path = parent.template.node_paths[dyn_index];
listeners.extend(
parent
.dynamic_attrs
.iter()
.enumerate()
.filter_map(|(idx, attr)| {
match is_path_ascendant(parent.template.node_paths[idx], path) {
true if attr.name == event.name => Some(attr),
_ => None,
}
}),
);
index = parent.parent;
}
for listener in listeners {
if let AttributeValue::Listener(listener) = &listener.value {
(listener.borrow_mut())(&event.event)
}
}
Some(())
}
/// Wait for the scheduler to have any work.
///
/// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when
/// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method
/// will exit.
///
/// This method is cancel-safe, so you're fine to discard the future in a select block.
///
/// This lets us poll async tasks during idle periods without blocking the main thread.
///
/// # Example
///
/// ```rust, ignore
/// let dom = VirtualDom::new(App);
/// let sender = dom.get_scheduler_channel();
/// ```
pub async fn wait_for_work(&mut self) {
let mut some_msg = None;
loop {
match some_msg.take() {
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
Some(msg) => match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
},
// If they're not ready, then we should wait for them to be ready
None => {
match self.rx.try_next() {
Ok(Some(val)) => some_msg = Some(val),
Ok(None) => return,
Err(_) => {
// If we have any dirty scopes, or finished fiber trees then we should exit
if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() {
return;
}
some_msg = self.rx.next().await
}
}
}
}
}
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
///
/// The diff machine expects the RealDom's stack to be the root of the application.
@@ -268,22 +400,71 @@ impl VirtualDom {
mutations
}
/// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
/// suspended subtrees.
pub fn render_immediate(&mut self) -> Mutations {
// Build a waker that won't wake up since our deadline is already expired when it's polled
let waker = futures_util::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
// Now run render with deadline but dont even try to poll any async tasks
let fut = self.render_with_deadline(std::future::ready(()));
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(mutations) => mutations,
std::task::Poll::Pending => panic!("render_immediate should never return pending"),
}
}
/// Render what you can given the timeline and then move on
///
/// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
pub async fn render_with_deadline(
&mut self,
///
/// If no suspense trees are present
pub async fn render_with_deadline<'a>(
&'a mut self,
deadline: impl Future<Output = ()>,
) -> Vec<Mutation> {
todo!()
}
) -> Mutations<'a> {
use futures_util::future::{select, Either};
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id.0)
}
let mut mutations = Mutations::new(0);
pin_mut!(deadline);
pub fn base_scope(&self) -> &ScopeState {
self.scopes.get(0).unwrap()
loop {
// first, unload any complete suspense trees
for finished_fiber in self.finished_fibers.drain(..) {
let scope = &mut self.scopes[finished_fiber.0];
let context = scope.has_context::<SuspenseContext>().unwrap();
let mut context = context.borrow_mut();
mutations.extend(context.mutations.drain(..));
}
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
self.dirty_scopes.remove(&dirty);
// if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update
if !self.is_scope_suspended(dirty.id) {
self.run_scope(dirty.id);
self.diff_scope(&mut mutations, dirty.id);
}
}
// Wait for suspense, or a deadline
if self.dirty_scopes.is_empty() {
if self.scheduler.leaves.borrow().is_empty() {
return mutations;
}
let (work, deadline) = (self.wait_for_work(), &mut deadline);
pin_mut!(work);
if let Either::Left((_, _)) = select(deadline, work).await {
return mutations;
}
}
}
}
}
-25
View File
@@ -1,25 +0,0 @@
use dioxus_core::prelude::*;
fn app(cx: Scope) -> Element {
todo!();
// render! {
// Suspend {
// delay: Duration::from_millis(100),
// fallback: rsx! { "Loading..." },
// ChildAsync {}
// ChildAsync {}
// ChildAsync {}
// }
// }
}
async fn ChildAsync(cx: Scope<'_>) -> Element {
todo!()
}
#[test]
fn it_works() {
let mut dom = VirtualDom::new(app);
let mut mutations = dom.rebuild();
}
+18 -15
View File
@@ -6,26 +6,29 @@ use dioxus_core::*;
async fn it_works() {
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
let _ = dom.rebuild();
println!("mutations: {:?}", mutations);
dom.wait_for_work().await;
tokio::select! {
_ = dom.wait_for_work() => {}
_ = tokio::time::sleep(Duration::from_millis(1000)) => {}
};
}
fn app(cx: Scope) -> Element {
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_millis(500)).await;
println!("Hello, world! {x}");
}
});
cx.use_hook(|| {
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_millis(50)).await;
println!("Hello, world! {x}");
}
});
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_millis(250)).await;
println!("Hello, world does! {x}");
}
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_millis(25)).await;
println!("Hello, world from second thread! {x}");
}
});
});
None
-3
View File
@@ -35,9 +35,6 @@ env_logger = "0.9.0"
tokio = { version = "1.21.2", features = ["full"] }
# dioxus-edit-stream = { path = "../edit-stream" }
[[bench]]
name = "create"
harness = false
[[bench]]
name = "jsframework"
-1
View File
@@ -1 +0,0 @@
fn main() {}
+21 -15
View File
@@ -5,9 +5,14 @@
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
//!
//! On my MBP 2019:
//! - Dioxus takes 3ms to create 1_000 rows
//! - Dioxus takes 30ms to create 10_000 rows
//!
//! Pre-templates (Mac M1):
//! - 3ms to create 1_000 rows
//! - 30ms to create 10_000 rows
//!
//! Post-templates
//! - 580us to create 1_000 rows
//! - 6.2ms to create 10_000 rows
//!
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
@@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows);
criterion_main!(mbenches);
fn create_rows(c: &mut Criterion) {
static App: Component = |cx| {
fn app(cx: Scope) -> Element {
let mut rng = SmallRng::from_entropy();
render!(table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!(Row {
row_id: f,
label: label
render!(
table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!( Row { row_id: f, label: label } )
})
})
}
}
})
};
)
}
c.bench_function("create rows", |b| {
let mut dom = VirtualDom::new(app);
dom.rebuild();
b.iter(|| {
let mut dom = VirtualDom::new(App);
let g = dom.rebuild();
assert!(g.edits.len() > 1);
})
+101
View File
@@ -0,0 +1,101 @@
use dioxus::prelude::*;
use rand::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
dom.rebuild();
for _ in 0..1000 {
dom.rebuild();
}
}
fn app(cx: Scope) -> Element {
let mut rng = SmallRng::from_entropy();
render! (
table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!( Row { row_id: f, label: label } )
})
}
}
)
}
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Label,
}
fn Row(cx: Scope<RowProps>) -> Element {
let [adj, col, noun] = cx.props.label.0;
render! {
tr {
td { class:"col-md-1", "{cx.props.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ },
a { class: "lbl", "{adj}" "{col}" "{noun}" }
}
td { class: "col-md-1",
a { class: "remove", onclick: move |_| {/* remove */},
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
}
}
#[derive(PartialEq)]
struct Label([&'static str; 3]);
impl Label {
fn new(rng: &mut SmallRng) -> Self {
Label([
ADJECTIVES.choose(rng).unwrap(),
COLOURS.choose(rng).unwrap(),
NOUNS.choose(rng).unwrap(),
])
}
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];
+56 -3
View File
@@ -1,5 +1,3 @@
use dioxus::prelude::*;
fn basic_syntax_is_a_template(cx: Scope) -> Element {
let asd = 123;
let var = 123;
@@ -23,8 +21,63 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
})
}
// imports come after the test since the rsx! macro is sensitive to its location in the file
// (the byte index is used to differentiate sub templates)
use dioxus::core::{ElementId, Mutation};
use dioxus::prelude::*;
#[test]
fn dual_stream() {
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
dbg!(dom.rebuild());
let edits = dom.rebuild();
use Mutation::*;
assert_eq!(
edits.template_mutations,
vec![
CreateElement { name: "div", namespace: None, id: ElementId(1) },
SetAttribute { name: "class", value: "asd", id: ElementId(1) },
CreateElement { name: "div", namespace: None, id: ElementId(2) },
CreatePlaceholder { id: ElementId(3) },
AppendChildren { m: 1 },
CreateElement { name: "div", namespace: None, id: ElementId(4) },
CreateElement { name: "h1", namespace: None, id: ElementId(5) },
CreateText { value: "var" },
AppendChildren { m: 1 },
CreateElement { name: "p", namespace: None, id: ElementId(6) },
CreateText { value: "you're great!" },
AppendChildren { m: 1 },
CreateElement { name: "div", namespace: None, id: ElementId(7) },
SetAttribute { name: "background-color", value: "red", id: ElementId(7) },
CreateElement { name: "h1", namespace: None, id: ElementId(8) },
CreateText { value: "var" },
AppendChildren { m: 1 },
CreateElement { name: "div", namespace: None, id: ElementId(9) },
CreateElement { name: "b", namespace: None, id: ElementId(10) },
CreateText { value: "asd" },
AppendChildren { m: 1 },
CreateText { value: "not great" },
AppendChildren { m: 2 },
AppendChildren { m: 2 },
CreateElement { name: "p", namespace: None, id: ElementId(11) },
CreateText { value: "you're great!" },
AppendChildren { m: 1 },
AppendChildren { m: 4 },
AppendChildren { m: 2 },
SaveTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", m: 1 }
]
);
assert_eq!(
edits.edits,
vec![
LoadTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", index: 0 },
AssignId { path: &[], id: ElementId(12) },
SetAttribute { name: "class", value: "123", id: ElementId(12) },
SetAttribute { name: "onclick", value: "asd", id: ElementId(12) }, // ---- todo: listeners
HydrateText { path: &[0, 0], value: "123", id: ElementId(13) },
ReplacePlaceholder { m: 1, path: &[0, 0] },
AppendChildren { m: 1 }
]
);
}
+1
View File
@@ -1,6 +1,7 @@
use std::future::IntoFuture;
use dioxus::prelude::*;
use dioxus_core::SuspenseBoundary;
#[inline_props]
fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
-1
View File
@@ -1 +0,0 @@
fn main() {}