mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-17 06:08:26 +00:00
feat: split apart template mutations
This commit is contained in:
parent
203935834d
commit
fc9fe6e560
21 changed files with 432 additions and 223 deletions
|
@ -4,6 +4,7 @@ use futures_util::Future;
|
|||
|
||||
use crate::{
|
||||
factory::{ComponentReturn, RenderReturn},
|
||||
innerlude::Scoped,
|
||||
scopes::{Scope, ScopeState},
|
||||
Element,
|
||||
};
|
||||
|
@ -69,10 +70,10 @@ impl<'a, P, A, F: ComponentReturn<'a, A>> AnyProps<'a> for VComponentProps<'a, P
|
|||
// Make sure the scope ptr is not null
|
||||
// self.props.state.set(scope);
|
||||
|
||||
let scope = Scope {
|
||||
let scope = cx.bump().alloc(Scoped {
|
||||
props: unsafe { &*self.props },
|
||||
scope: cx,
|
||||
};
|
||||
});
|
||||
|
||||
// Call the render function directly
|
||||
(self.render_fn)(scope).as_return(cx)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{nodes::VNode, virtualdom::VirtualDom};
|
||||
use crate::{nodes::VNode, virtual_dom::VirtualDom};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct ElementId(pub usize);
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
use std::pin::Pin;
|
||||
|
||||
use crate::factory::{FiberLeaf, RenderReturn};
|
||||
use crate::innerlude::SuspenseContext;
|
||||
use crate::innerlude::{Renderer, SuspenseContext};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::mutations::Mutation::*;
|
||||
use crate::nodes::VNode;
|
||||
use crate::nodes::{DynamicNode, TemplateNode};
|
||||
use crate::virtualdom::VirtualDom;
|
||||
use crate::virtual_dom::VirtualDom;
|
||||
use crate::{AttributeValue, Element, ElementId, TemplateAttribute};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use futures_util::Future;
|
||||
|
||||
impl VirtualDom {
|
||||
/// Create this template and write its mutations
|
||||
pub fn create<'a>(
|
||||
&mut self,
|
||||
mutations: &mut Vec<Mutation<'a>>,
|
||||
template: &'a VNode<'a>,
|
||||
) -> usize {
|
||||
pub fn create<'a>(&mut self, mutations: &mut Renderer<'a>, template: &'a VNode<'a>) -> usize {
|
||||
// The best renderers will have templates prehydrated
|
||||
// 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 {
|
||||
let mutations = &mut mutations.template_mutations;
|
||||
self.create_static_node(mutations, template, node);
|
||||
}
|
||||
|
||||
mutations.push(SaveTemplate {
|
||||
mutations.template_mutations.push(SaveTemplate {
|
||||
name: template.template.id,
|
||||
m: template.template.roots.len(),
|
||||
});
|
||||
|
@ -162,7 +159,7 @@ impl VirtualDom {
|
|||
|
||||
pub fn create_dynamic_node<'a>(
|
||||
&mut self,
|
||||
mutations: &mut Vec<Mutation<'a>>,
|
||||
mutations: &mut Renderer<'a>,
|
||||
template: &'a VNode<'a>,
|
||||
node: &'a DynamicNode<'a>,
|
||||
idx: usize,
|
||||
|
@ -183,44 +180,61 @@ impl VirtualDom {
|
|||
DynamicNode::Component {
|
||||
props, placeholder, ..
|
||||
} => {
|
||||
println!("creaitng component");
|
||||
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) };
|
||||
|
||||
// if boundary or subtree, start working on a new stack of mutations
|
||||
|
||||
match render_ret {
|
||||
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));
|
||||
mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
});
|
||||
0
|
||||
}
|
||||
|
||||
RenderReturn::Sync(Some(template)) => {
|
||||
let mutations_to_this_point = mutations.len();
|
||||
|
||||
self.scope_stack.push(id);
|
||||
let created = self.create(mutations, template);
|
||||
let mut created = self.create(mutations, template);
|
||||
self.scope_stack.pop();
|
||||
|
||||
if !self.waiting_on.is_empty() {
|
||||
if let Some(boundary) =
|
||||
self.scopes[id.0].has_context::<SuspenseContext>()
|
||||
{
|
||||
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(..));
|
||||
|
||||
// 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));
|
||||
mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
});
|
||||
created = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// handle any waiting on futures accumulated by async calls down the tree
|
||||
// if this is a boundary, we split off the tree
|
||||
created
|
||||
}
|
||||
|
||||
// whenever the future is polled later, we'll revisit it
|
||||
// For now, just set the placeholder
|
||||
RenderReturn::Sync(None) => {
|
||||
let new_id = self.next_element(template);
|
||||
placeholder.set(Some(new_id));
|
||||
self.scopes[id.0].placeholder.set(Some(new_id));
|
||||
mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
});
|
||||
0
|
||||
}
|
||||
|
||||
RenderReturn::Async(fut) => {
|
||||
let new_id = self.next_element(template);
|
||||
placeholder.set(Some(new_id));
|
||||
self.scopes[id.0].placeholder.set(Some(new_id));
|
||||
mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
});
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crate::virtualdom::VirtualDom;
|
||||
use crate::innerlude::Renderer;
|
||||
use crate::virtual_dom::VirtualDom;
|
||||
use crate::{Attribute, AttributeValue, TemplateNode};
|
||||
|
||||
use crate::any_props::VComponentProps;
|
||||
|
@ -25,13 +26,13 @@ pub struct DirtyScope {
|
|||
}
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
pub fn diff_scope(&mut self, mutations: &mut Vec<Mutation<'b>>, scope: ScopeId) {
|
||||
pub fn diff_scope(&mut self, mutations: &mut Renderer<'b>, scope: ScopeId) {
|
||||
let scope_state = &mut self.scopes[scope.0];
|
||||
}
|
||||
|
||||
pub fn diff_node(
|
||||
&mut self,
|
||||
muts: &mut Vec<Mutation<'b>>,
|
||||
muts: &mut Renderer<'b>,
|
||||
left_template: &'b VNode<'b>,
|
||||
right_template: &'b VNode<'b>,
|
||||
) {
|
||||
|
@ -176,7 +177,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 Vec<Mutation<'b>>,
|
||||
muts: &mut Renderer<'b>,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
) {
|
||||
|
@ -216,7 +217,7 @@ impl<'b> VirtualDom {
|
|||
// The stack is empty upon entry.
|
||||
fn diff_keyed_children(
|
||||
&mut self,
|
||||
muts: &mut Vec<Mutation<'b>>,
|
||||
muts: &mut Renderer<'b>,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
) {
|
||||
|
@ -295,7 +296,7 @@ impl<'b> VirtualDom {
|
|||
// /// If there is no offset, then this function returns None and the diffing is complete.
|
||||
// fn diff_keyed_ends(
|
||||
// &mut self,
|
||||
// muts: &mut Vec<Mutation<'b>>,
|
||||
// muts: &mut Renderer<'b>,
|
||||
// old: &'b [VNode<'b>],
|
||||
// new: &'b [VNode<'b>],
|
||||
// ) -> Option<(usize, usize)> {
|
||||
|
@ -354,7 +355,7 @@ impl<'b> VirtualDom {
|
|||
// #[allow(clippy::too_many_lines)]
|
||||
// fn diff_keyed_middle(
|
||||
// &mut self,
|
||||
// muts: &mut Vec<Mutation<'b>>,
|
||||
// muts: &mut Renderer<'b>,
|
||||
// old: &'b [VNode<'b>],
|
||||
// new: &'b [VNode<'b>],
|
||||
// ) {
|
||||
|
@ -532,7 +533,7 @@ impl<'b> VirtualDom {
|
|||
|
||||
/// Remove these nodes from the dom
|
||||
/// Wont generate mutations for the inner nodes
|
||||
fn remove_nodes(&mut self, muts: &mut Vec<Mutation<'b>>, nodes: &'b [VNode<'b>]) {
|
||||
fn remove_nodes(&mut self, muts: &mut Renderer<'b>, nodes: &'b [VNode<'b>]) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{arena::ElementId, virtualdom::VirtualDom, Attribute, AttributeValue};
|
||||
use crate::{arena::ElementId, virtual_dom::VirtualDom, Attribute, AttributeValue};
|
||||
use std::cell::Cell;
|
||||
|
||||
/// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{nodes::VNode, scopes::ScopeId, virtualdom::VirtualDom, DynamicNode};
|
||||
use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode};
|
||||
|
||||
impl VirtualDom {
|
||||
pub fn drop_scope(&mut self, id: ScopeId) {
|
||||
|
|
|
@ -14,7 +14,7 @@ mod properties;
|
|||
mod scheduler;
|
||||
mod scope_arena;
|
||||
mod scopes;
|
||||
mod virtualdom;
|
||||
mod virtual_dom;
|
||||
|
||||
pub(crate) mod innerlude {
|
||||
pub use crate::arena::*;
|
||||
|
@ -25,7 +25,7 @@ pub(crate) mod innerlude {
|
|||
pub use crate::properties::*;
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scopes::*;
|
||||
pub use crate::virtualdom::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
|
||||
///
|
||||
|
@ -83,6 +83,9 @@ pub use crate::innerlude::{
|
|||
Scope,
|
||||
ScopeId,
|
||||
ScopeState,
|
||||
Scoped,
|
||||
SuspenseBoundary,
|
||||
SuspenseContext,
|
||||
TaskId,
|
||||
Template,
|
||||
TemplateAttribute,
|
||||
|
@ -98,8 +101,8 @@ pub use crate::innerlude::{
|
|||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, Attribute, DynamicNode, Element, EventPriority, LazyNodes, NodeFactory,
|
||||
Properties, Scope, ScopeId, ScopeState, TaskId, Template, TemplateAttribute, TemplateNode,
|
||||
UiEvent, VNode, VirtualDom,
|
||||
Properties, Scope, ScopeId, ScopeState, Scoped, SuspenseBoundary, SuspenseContext, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -132,3 +135,24 @@ 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 {
|
||||
() => {};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,81 @@
|
|||
use crate::arena::ElementId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Renderer<'a> {
|
||||
mutations: Vec<Mutation<'a>>,
|
||||
pub subtree: usize,
|
||||
pub mutations: Vec<Mutation<'a>>,
|
||||
pub template_mutations: Vec<Mutation<'a>>,
|
||||
// mutations: Vec<Mutations<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub fn new(subtree: usize) -> Self {
|
||||
Self {
|
||||
subtree,
|
||||
mutations: Vec::new(),
|
||||
template_mutations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for Renderer<'a> {
|
||||
type Target = Vec<Mutation<'a>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mutations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Renderer<'_> {
|
||||
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<Item = Mutation<'a>>) {
|
||||
// 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<Mutation<'a>>,
|
||||
// }
|
||||
|
||||
/*
|
||||
each subtree has its own numbering scheme
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Mutation<'a> {
|
||||
SetAttribute {
|
||||
|
|
|
@ -6,7 +6,10 @@ use std::{
|
|||
};
|
||||
|
||||
use super::{waker::RcWake, SchedulerMsg};
|
||||
use crate::{innerlude::Mutation, Element, ScopeId};
|
||||
use crate::{
|
||||
innerlude::{Mutation, Renderer},
|
||||
Element, ScopeId,
|
||||
};
|
||||
use futures_task::Waker;
|
||||
use futures_util::Future;
|
||||
|
||||
|
@ -14,27 +17,27 @@ use futures_util::Future;
|
|||
pub struct SuspenseId(pub usize);
|
||||
|
||||
pub type SuspenseContext = Rc<RefCell<SuspenseBoundary>>;
|
||||
|
||||
/// Essentially a fiber in React
|
||||
pub struct SuspenseBoundary {
|
||||
pub id: ScopeId,
|
||||
pub waiting_on: HashSet<SuspenseId>,
|
||||
pub mutations: Vec<Mutation<'static>>,
|
||||
pub mutations: Renderer<'static>,
|
||||
}
|
||||
|
||||
impl SuspenseBoundary {
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
pub fn new(id: ScopeId) -> Rc<RefCell<Self>> {
|
||||
Rc::new(RefCell::new(Self {
|
||||
id,
|
||||
waiting_on: Default::default(),
|
||||
mutations: Default::default(),
|
||||
}
|
||||
mutations: Renderer::new(0),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuspenseLeaf {
|
||||
pub id: SuspenseId,
|
||||
pub scope_id: ScopeId,
|
||||
pub boundary: ScopeId,
|
||||
pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub notified: Cell<bool>,
|
||||
|
||||
|
@ -43,10 +46,10 @@ pub struct SuspenseLeaf {
|
|||
|
||||
impl RcWake for SuspenseLeaf {
|
||||
fn wake_by_ref(arc_self: &Rc<Self>) {
|
||||
if arc_self.notified.get() {
|
||||
return;
|
||||
}
|
||||
arc_self.notified.set(true);
|
||||
// if arc_self.notified.get() {
|
||||
// return;
|
||||
// }
|
||||
// arc_self.notified.set(true);
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
|
||||
|
|
|
@ -3,7 +3,7 @@ use futures_util::{FutureExt, StreamExt};
|
|||
|
||||
use crate::{
|
||||
factory::RenderReturn,
|
||||
innerlude::{Mutation, SuspenseContext},
|
||||
innerlude::{Mutation, Renderer, SuspenseContext},
|
||||
VNode, VirtualDom,
|
||||
};
|
||||
|
||||
|
@ -38,6 +38,8 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
SchedulerMsg::SuspenseNotified(id) => {
|
||||
println!("suspense notified");
|
||||
|
||||
let leaf = self
|
||||
.scheduler
|
||||
.handle
|
||||
|
@ -63,11 +65,19 @@ impl VirtualDom {
|
|||
// 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)
|
||||
{
|
||||
let boundary = &self.scopes[leaf.boundary.0]
|
||||
let boundary = &self.scopes[leaf.scope_id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.unwrap();
|
||||
|
||||
println!("ready pool");
|
||||
|
||||
let mut fiber = boundary.borrow_mut();
|
||||
|
||||
println!(
|
||||
"Existing mutations {:?}, scope {:?}",
|
||||
fiber.mutations, fiber.id
|
||||
);
|
||||
|
||||
let scope = &mut self.scopes[scope_id.0];
|
||||
let arena = scope.current_arena();
|
||||
|
||||
|
@ -77,18 +87,24 @@ 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 Vec<Mutation> =
|
||||
let mutations: &mut Renderer =
|
||||
unsafe { std::mem::transmute(mutations) };
|
||||
|
||||
self.scope_stack.push(scope_id);
|
||||
self.create(mutations, template);
|
||||
self.scope_stack.pop();
|
||||
|
||||
println!("{:?}", mutations);
|
||||
println!("{:#?}", mutations);
|
||||
} else {
|
||||
println!("nodes arent right");
|
||||
}
|
||||
} else {
|
||||
println!("not ready");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now proces any events. If we end up running a component and it generates mutations, then we should run those mutations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
factory::RenderReturn,
|
||||
innerlude::{SuspenseId, SuspenseLeaf},
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtualdom::VirtualDom,
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
|
||||
impl VirtualDom {
|
||||
|
@ -77,13 +77,13 @@ impl VirtualDom {
|
|||
let mut leaves = self.scheduler.handle.leaves.borrow_mut();
|
||||
let entry = leaves.vacant_entry();
|
||||
let key = entry.key();
|
||||
let suspense_id = SuspenseId(key);
|
||||
|
||||
let leaf = Rc::new(SuspenseLeaf {
|
||||
scope_id,
|
||||
task: task.as_mut(),
|
||||
id: SuspenseId(key),
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.handle.sender.clone(),
|
||||
boundary: ScopeId(0),
|
||||
notified: false.into(),
|
||||
});
|
||||
|
||||
|
@ -112,6 +112,7 @@ impl VirtualDom {
|
|||
// Insert the future into fiber leaves and break
|
||||
_ => {
|
||||
entry.insert(leaf);
|
||||
self.waiting_on.push(suspense_id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,23 +19,14 @@ use crate::{
|
|||
TaskId,
|
||||
};
|
||||
|
||||
pub struct Scope<'a, T = ()> {
|
||||
pub type Scope<'a, T = ()> = &'a Scoped<'a, T>;
|
||||
|
||||
pub struct Scoped<'a, T = ()> {
|
||||
pub scope: &'a ScopeState,
|
||||
pub props: &'a T,
|
||||
}
|
||||
|
||||
impl<T> Copy for Scope<'_, T> {}
|
||||
|
||||
impl<T> Clone for Scope<'_, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
props: self.props,
|
||||
scope: self.scope,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::Deref for Scope<'a, T> {
|
||||
impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
||||
type Target = &'a ScopeState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
@ -298,6 +289,19 @@ impl ScopeState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
match self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
Some(shared) => Some(
|
||||
(*shared
|
||||
.downcast_ref::<T>()
|
||||
.expect("Context of type T should exist"))
|
||||
.clone(),
|
||||
),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
self.tasks.spawn(self.id, fut)
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::arena::ElementPath;
|
|||
use crate::component::Component;
|
||||
use crate::diff::DirtyScope;
|
||||
use crate::factory::RenderReturn;
|
||||
use crate::innerlude::{Scheduler, SchedulerMsg};
|
||||
use crate::innerlude::{Renderer, Scheduler, SchedulerMsg};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::nodes::{Template, TemplateId};
|
||||
|
||||
|
@ -13,20 +13,22 @@ use crate::{
|
|||
};
|
||||
use crate::{scheduler, Element, Scope};
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use scheduler::{SuspenseBoundary, SuspenseContext};
|
||||
use futures_util::Future;
|
||||
use scheduler::{SuspenseBoundary, SuspenseContext, SuspenseId};
|
||||
use slab::Slab;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct VirtualDom {
|
||||
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
|
||||
pub(crate) elements: Slab<ElementPath>,
|
||||
pub(crate) scopes: Slab<ScopeState>,
|
||||
pub(crate) scope_stack: Vec<ScopeId>,
|
||||
pub(crate) element_stack: Vec<ElementId>,
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
pub(crate) scheduler: Scheduler,
|
||||
|
||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
||||
pub(crate) scope_stack: Vec<ScopeId>,
|
||||
pub(crate) waiting_on: Vec<SuspenseId>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
|
@ -40,6 +42,7 @@ impl VirtualDom {
|
|||
scope_stack: Vec::new(),
|
||||
element_stack: vec![ElementId(0)],
|
||||
dirty_scopes: BTreeSet::new(),
|
||||
waiting_on: Vec::new(),
|
||||
scheduler,
|
||||
};
|
||||
|
||||
|
@ -49,21 +52,25 @@ impl VirtualDom {
|
|||
let root = res.new_scope(props);
|
||||
|
||||
// the root component is always a suspense boundary for any async children
|
||||
res.scopes[root.0].provide_context(Rc::new(RefCell::new(SuspenseBoundary::new(root))));
|
||||
|
||||
res.scopes[root.0].provide_context(SuspenseBoundary::new(root));
|
||||
assert_eq!(root, ScopeId(0));
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Render the virtualdom, without processing any suspense.
|
||||
pub fn rebuild<'a>(&'a mut self, mutations: &mut Vec<Mutation<'a>>) {
|
||||
///
|
||||
/// 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) };
|
||||
|
||||
let mut created = 0;
|
||||
match root_node {
|
||||
RenderReturn::Sync(Some(node)) => {
|
||||
self.scope_stack.push(ScopeId(0));
|
||||
self.create(mutations, node);
|
||||
created = self.create(&mut mutations, node);
|
||||
self.scope_stack.pop();
|
||||
}
|
||||
RenderReturn::Sync(None) => {
|
||||
|
@ -71,22 +78,22 @@ impl VirtualDom {
|
|||
}
|
||||
RenderReturn::Async(_) => unreachable!(),
|
||||
}
|
||||
|
||||
mutations.push(Mutation::AppendChildren { m: created });
|
||||
|
||||
mutations
|
||||
}
|
||||
|
||||
/// Render what you can given the timeline and then move on
|
||||
pub async fn render_with_deadline<'a>(
|
||||
&'a mut self,
|
||||
future: impl std::future::Future<Output = ()>,
|
||||
mutations: &mut Vec<Mutation<'a>>,
|
||||
) {
|
||||
///
|
||||
/// 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,
|
||||
deadline: impl Future<Output = ()>,
|
||||
) -> Vec<Mutation> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// Whenever the future is canceled, the VirtualDom will be
|
||||
pub async fn render<'a>(&'a mut self, mutations: &mut Vec<Mutation<'a>>) {
|
||||
//
|
||||
}
|
||||
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.scopes.get(id.0)
|
||||
}
|
|
@ -21,6 +21,5 @@ async fn ChildAsync(cx: Scope<'_>) -> Element {
|
|||
fn it_works() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mut mutations = vec![];
|
||||
dom.rebuild(&mut mutations);
|
||||
let mut mutations = dom.rebuild();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use std::{cell::Cell, ptr::null_mut, time::Duration};
|
||||
|
||||
use dioxus_core::*;
|
||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mut mutations = vec![];
|
||||
dom.rebuild(&mut mutations);
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
println!("mutations: {:?}", mutations);
|
||||
|
||||
|
@ -15,8 +13,23 @@ async fn it_works() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dy = cx.component(async_child, (), "async_child");
|
||||
VNode::single_component(&cx, dy, "app")
|
||||
println!("running root app");
|
||||
|
||||
VNode::single_component(
|
||||
cx,
|
||||
cx.component(suspense_boundary, (), "suspense_boundary"),
|
||||
"app",
|
||||
)
|
||||
}
|
||||
|
||||
fn suspense_boundary(cx: Scope) -> Element {
|
||||
println!("running boundary");
|
||||
|
||||
let _ = cx.use_hook(|| {
|
||||
cx.provide_context(Rc::new(RefCell::new(SuspenseBoundary::new(cx.scope_id()))))
|
||||
});
|
||||
|
||||
VNode::single_component(cx, cx.component(async_child, (), "async_child"), "app")
|
||||
}
|
||||
|
||||
async fn async_child(cx: Scope<'_>) -> Element {
|
||||
|
@ -34,8 +47,9 @@ async fn async_child(cx: Scope<'_>) -> Element {
|
|||
|
||||
println!("Future awaited and complete");
|
||||
|
||||
let dy = cx.component(async_child, (), "async_child");
|
||||
VNode::single_component(&cx, dy, "app")
|
||||
|
||||
// VNode::single_text(&cx, &[TemplateNode::Text("it works!")], "beauty")
|
||||
VNode::single_component(cx, cx.component(async_text, (), "async_text"), "app")
|
||||
}
|
||||
|
||||
async fn async_text(cx: Scope<'_>) -> Element {
|
||||
VNode::single_text(&cx, &[TemplateNode::Text("it works!")], "beauty")
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@ use dioxus_core::*;
|
|||
async fn it_works() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mut mutations = vec![];
|
||||
dom.rebuild(&mut mutations);
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
println!("mutations: {:?}", mutations);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ rand = { version = "0.8.4", features = ["small_rng"] }
|
|||
criterion = "0.3.5"
|
||||
thiserror = "1.0.30"
|
||||
env_logger = "0.9.0"
|
||||
tokio = { version = "1.21.2", features = ["full"] }
|
||||
# dioxus-edit-stream = { path = "../edit-stream" }
|
||||
|
||||
[[bench]]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::future::IntoFuture;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||
|
@ -23,41 +25,63 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
|||
})
|
||||
}
|
||||
|
||||
fn basic_template(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
basic_child { }
|
||||
async_child { }
|
||||
}
|
||||
})
|
||||
#[inline_props]
|
||||
fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
|
||||
cx.use_hook(|| cx.provide_context(SuspenseBoundary::new(cx.scope_id())));
|
||||
cx.render(rsx! { children })
|
||||
}
|
||||
|
||||
fn basic_child(cx: Scope) -> Element {
|
||||
todo!()
|
||||
cx.render(rsx! {
|
||||
div { "basic child 1" }
|
||||
})
|
||||
}
|
||||
|
||||
async fn async_child(cx: Scope<'_>) -> Element {
|
||||
todo!()
|
||||
let username = use_future!(cx, || async {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
"async child 1"
|
||||
});
|
||||
|
||||
let age = use_future!(cx, || async {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
println!("long future completed");
|
||||
1234
|
||||
});
|
||||
|
||||
let (_user, _age) = use_future!(cx, || async {
|
||||
tokio::join!(
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)),
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2))
|
||||
);
|
||||
("async child 1", 1234)
|
||||
})
|
||||
.await;
|
||||
|
||||
let (username, age) = tokio::join!(username.into_future(), age.into_future());
|
||||
|
||||
cx.render(rsx!(
|
||||
div { "Hello! {username}, you are {age}, {_user} {_age}" }
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_prints() {
|
||||
let mut dom = VirtualDom::new(basic_template);
|
||||
#[tokio::test]
|
||||
async fn basic_prints() {
|
||||
let mut dom = VirtualDom::new(|cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "var" }
|
||||
suspense_boundary {
|
||||
basic_child { }
|
||||
async_child { }
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut edits = Vec::new();
|
||||
dom.rebuild(&mut edits);
|
||||
dbg!(edits);
|
||||
dbg!(dom.rebuild());
|
||||
|
||||
let mut edits = Vec::new();
|
||||
dom.rebuild(&mut edits);
|
||||
dom.wait_for_work().await;
|
||||
|
||||
dbg!(edits);
|
||||
// let renderer = dioxus_edit_stream::Mutations::default();
|
||||
//
|
||||
// dbg!(renderer.edits);
|
||||
|
||||
// takes_it(basic_child);
|
||||
dbg!(dom.rebuild());
|
||||
}
|
||||
|
||||
// fn takes_it(f: fn(Scope) -> Element) {}
|
||||
// fn takes_it(f: fn(Scope) -> Element) {}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
#![allow(missing_docs)]
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
future::{Future, IntoFuture},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A future that resolves to a value.
|
||||
///
|
||||
|
@ -28,16 +34,13 @@ where
|
|||
let state = cx.use_hook(move || UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
slot: Rc::new(Cell::new(None)),
|
||||
value: None,
|
||||
values: Default::default(),
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
waker: Default::default(),
|
||||
});
|
||||
|
||||
if let Some(value) = state.slot.take() {
|
||||
state.value = Some(value);
|
||||
state.task.set(None);
|
||||
}
|
||||
*state.waker.borrow_mut() = None;
|
||||
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() {
|
||||
// We don't need regen anymore
|
||||
|
@ -47,8 +50,9 @@ where
|
|||
let fut = future(dependencies.out());
|
||||
|
||||
// Clone in our cells
|
||||
let slot = state.slot.clone();
|
||||
let values = state.values.clone();
|
||||
let schedule_update = state.update.clone();
|
||||
let waker = state.waker.clone();
|
||||
|
||||
// Cancel the current future
|
||||
if let Some(current) = state.task.take() {
|
||||
|
@ -57,8 +61,16 @@ where
|
|||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
let res = fut.await;
|
||||
slot.set(Some(res));
|
||||
schedule_update();
|
||||
values.borrow_mut().push(Box::leak(Box::new(res)));
|
||||
|
||||
// if there's a waker, we dont re-render the component. Instead we just progress that future
|
||||
match waker.borrow().as_ref() {
|
||||
Some(waker) => waker.wake_by_ref(),
|
||||
None => {
|
||||
println!("scheduling update");
|
||||
// schedule_update()
|
||||
}
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
|
@ -74,10 +86,10 @@ pub enum FutureState<'a, T> {
|
|||
pub struct UseFuture<T> {
|
||||
update: Arc<dyn Fn()>,
|
||||
needs_regen: Cell<bool>,
|
||||
value: Option<T>,
|
||||
slot: Rc<Cell<Option<T>>>,
|
||||
task: Cell<Option<TaskId>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
waker: Rc<RefCell<Option<std::task::Waker>>>,
|
||||
values: Rc<RefCell<Vec<*mut T>>>,
|
||||
}
|
||||
|
||||
pub enum UseFutureState<'a, T> {
|
||||
|
@ -105,22 +117,25 @@ impl<T> UseFuture<T> {
|
|||
|
||||
// clears the value in the future slot without starting the future over
|
||||
pub fn clear(&self) -> Option<T> {
|
||||
(self.update)();
|
||||
self.slot.replace(None)
|
||||
todo!()
|
||||
// (self.update)();
|
||||
// self.slot.replace(None)
|
||||
}
|
||||
|
||||
// Manually set the value in the future slot without starting the future over
|
||||
pub fn set(&self, new_value: T) {
|
||||
self.slot.set(Some(new_value));
|
||||
self.needs_regen.set(true);
|
||||
(self.update)();
|
||||
// self.slot.set(Some(new_value));
|
||||
// self.needs_regen.set(true);
|
||||
// (self.update)();
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Return any value, even old values if the future has not yet resolved.
|
||||
///
|
||||
/// If the future has never completed, the returned value will be `None`.
|
||||
pub fn value(&self) -> Option<&T> {
|
||||
self.value.as_ref()
|
||||
// self.value.as_ref()
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the ID of the future in Dioxus' internal scheduler
|
||||
|
@ -130,18 +145,49 @@ impl<T> UseFuture<T> {
|
|||
|
||||
/// Get the current stateof the future.
|
||||
pub fn state(&self) -> UseFutureState<T> {
|
||||
match (&self.task.get(), &self.value) {
|
||||
// If we have a task and an existing value, we're reloading
|
||||
(Some(_), Some(val)) => UseFutureState::Reloading(val),
|
||||
todo!()
|
||||
// match (&self.task.get(), &self.value) {
|
||||
// // If we have a task and an existing value, we're reloading
|
||||
// (Some(_), Some(val)) => UseFutureState::Reloading(val),
|
||||
|
||||
// no task, but value - we're done
|
||||
(None, Some(val)) => UseFutureState::Complete(val),
|
||||
// // no task, but value - we're done
|
||||
// (None, Some(val)) => UseFutureState::Complete(val),
|
||||
|
||||
// no task, no value - something's wrong? return pending
|
||||
(None, None) => UseFutureState::Pending,
|
||||
// // no task, no value - something's wrong? return pending
|
||||
// (None, None) => UseFutureState::Pending,
|
||||
|
||||
// Task, no value - we're still pending
|
||||
(Some(_), None) => UseFutureState::Pending,
|
||||
// // Task, no value - we're still pending
|
||||
// (Some(_), None) => UseFutureState::Pending,
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoFuture for &'a UseFuture<T> {
|
||||
type Output = &'a T;
|
||||
type IntoFuture = UseFutureAwait<'a, T>;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
UseFutureAwait { hook: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UseFutureAwait<'a, T> {
|
||||
hook: &'a UseFuture<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Future for UseFutureAwait<'a, T> {
|
||||
type Output = &'a T;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
println!("polling future");
|
||||
match self.hook.values.borrow_mut().last().cloned() {
|
||||
Some(value) => std::task::Poll::Ready(unsafe { &*value }),
|
||||
None => {
|
||||
self.hook.waker.replace(Some(cx.waker().clone()));
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +285,18 @@ impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f,);
|
|||
impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g,);
|
||||
impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g, H = h,);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! use_future {
|
||||
($cx:ident, || $($rest:tt)*) => { use_future( $cx, (), |_| $($rest)* ) };
|
||||
($cx:ident, | $($args:tt),* | $($rest:tt)*) => {
|
||||
use_future(
|
||||
$cx,
|
||||
($($args),*),
|
||||
|($($args),*)| $($rest)*
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -256,22 +314,32 @@ mod tests {
|
|||
e: i32,
|
||||
}
|
||||
|
||||
fn app(cx: Scope<MyProps>) -> Element {
|
||||
async fn app(cx: Scope<'_, MyProps>) -> Element {
|
||||
// should only ever run once
|
||||
let fut = use_future(&cx, (), |_| async move {
|
||||
//
|
||||
});
|
||||
let fut = use_future(cx, (), |_| async move {});
|
||||
|
||||
// runs when a is changed
|
||||
let fut = use_future(&cx, (&cx.props.a,), |(a,)| async move {
|
||||
//
|
||||
});
|
||||
let fut = use_future(cx, (&cx.props.a,), |(a,)| async move {});
|
||||
|
||||
// runs when a or b is changed
|
||||
let fut = use_future(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move {
|
||||
//
|
||||
let fut = use_future(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { 123 });
|
||||
|
||||
let a = use_future!(cx, || async move {
|
||||
// do the thing!
|
||||
});
|
||||
|
||||
let b = &123;
|
||||
let c = &123;
|
||||
|
||||
let a = use_future!(cx, |b, c| async move {
|
||||
let a = b + c;
|
||||
let blah = "asd";
|
||||
});
|
||||
|
||||
let g2 = a.await;
|
||||
|
||||
let g = fut.await;
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -472,6 +472,8 @@ fn api_makes_sense() {
|
|||
}
|
||||
});
|
||||
|
||||
cx.render(LazyNodes::new(|f| f.static_text("asd")))
|
||||
// cx.render(LazyNodes::new(|f| f.static_text("asd")))
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
use std::{cell::Cell, future::Future, rc::Rc};
|
||||
|
||||
use dioxus_core::{Element, ScopeState, TaskId};
|
||||
|
||||
pub fn use_suspense<R: 'static, F: Future<Output = R> + 'static>(
|
||||
cx: &ScopeState,
|
||||
create_future: impl FnOnce() -> F,
|
||||
render: impl FnOnce(&R) -> Element,
|
||||
) -> Element {
|
||||
let sus = cx.use_hook(|| {
|
||||
let fut = create_future();
|
||||
|
||||
let wip_value: Rc<Cell<Option<R>>> = Default::default();
|
||||
|
||||
let wip = wip_value.clone();
|
||||
let new_fut = async move {
|
||||
let val = fut.await;
|
||||
wip.set(Some(val));
|
||||
};
|
||||
|
||||
let task = cx.push_future(new_fut);
|
||||
SuspenseInner {
|
||||
_task: task,
|
||||
value: None,
|
||||
_wip_value: wip_value,
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(value) = sus.value.as_ref() {
|
||||
render(value)
|
||||
} else {
|
||||
// generate a placeholder node if the future isnt ready
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct SuspenseInner<R> {
|
||||
_task: TaskId,
|
||||
_wip_value: Rc<Cell<Option<R>>>,
|
||||
value: Option<R>,
|
||||
}
|
Loading…
Add table
Reference in a new issue