chore: cleanup src
This commit is contained in:
1 parent
77686ba329
commit
f2e343c154
16 files changed
+43
-1479
No files matched your search
@@ -138,6 +138,5 @@ impl EmptyBuilder {
|
|||||||
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
|
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
|
||||||
/// to initialize a component's props.
|
/// to initialize a component's props.
|
||||||
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Context<'a>, &'a T) -> Element) -> T::Builder {
|
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Context<'a>, &'a T) -> Element) -> T::Builder {
|
||||||
// pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder {
|
|
||||||
T::builder()
|
T::builder()
|
||||||
}
|
}
|
||||||
@@ -124,32 +124,6 @@ impl<'bump> DiffState<'bump> {
|
|||||||
force_diff: false,
|
force_diff: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_remove(&self, id: &ScopeId) -> Option<Scope> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reserve_node(&self, node: &VNode) -> ElementId {
|
|
||||||
todo!()
|
|
||||||
// let mut els = self.nodes.borrow_mut();
|
|
||||||
// let entry = els.vacant_entry();
|
|
||||||
// let key = entry.key();
|
|
||||||
// let id = ElementId(key);
|
|
||||||
// let node = node as *const _;
|
|
||||||
// let node = unsafe { std::mem::transmute(node) };
|
|
||||||
// entry.insert(node);
|
|
||||||
// id
|
|
||||||
|
|
||||||
// let nodes = self.nodes.borrow_mut();
|
|
||||||
// let id = nodes.insert(());
|
|
||||||
// let node_id = ElementId(id);
|
|
||||||
// node = Some(node_id);
|
|
||||||
// node_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_garbage(&self, id: ElementId) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'bump> DiffState<'bump> {
|
impl<'bump> DiffState<'bump> {
|
||||||
@@ -283,14 +257,14 @@ impl<'bump> DiffState<'bump> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
|
fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
|
||||||
let real_id = self.reserve_node(node);
|
let real_id = self.scopes.reserve_node(node);
|
||||||
self.mutations.create_text_node(vtext.text, real_id);
|
self.mutations.create_text_node(vtext.text, real_id);
|
||||||
vtext.dom_id.set(Some(real_id));
|
vtext.dom_id.set(Some(real_id));
|
||||||
self.stack.add_child_count(1);
|
self.stack.add_child_count(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) {
|
fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) {
|
||||||
let real_id = self.reserve_node(node);
|
let real_id = self.scopes.reserve_node(node);
|
||||||
self.mutations.create_placeholder(real_id);
|
self.mutations.create_placeholder(real_id);
|
||||||
|
|
||||||
suspended.dom_id.set(Some(real_id));
|
suspended.dom_id.set(Some(real_id));
|
||||||
@@ -300,7 +274,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) {
|
fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) {
|
||||||
let real_id = self.reserve_node(node);
|
let real_id = self.scopes.reserve_node(node);
|
||||||
self.mutations.create_placeholder(real_id);
|
self.mutations.create_placeholder(real_id);
|
||||||
anchor.dom_id.set(Some(real_id));
|
anchor.dom_id.set(Some(real_id));
|
||||||
self.stack.add_child_count(1);
|
self.stack.add_child_count(1);
|
||||||
@@ -317,7 +291,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
..
|
..
|
||||||
} = element;
|
} = element;
|
||||||
|
|
||||||
let real_id = self.reserve_node(node);
|
let real_id = self.scopes.reserve_node(node);
|
||||||
|
|
||||||
dom_id.set(Some(real_id));
|
dom_id.set(Some(real_id));
|
||||||
|
|
||||||
@@ -1142,7 +1116,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
match node {
|
match node {
|
||||||
VNode::Text(t) => {
|
VNode::Text(t) => {
|
||||||
let id = t.dom_id.get().unwrap();
|
let id = t.dom_id.get().unwrap();
|
||||||
self.collect_garbage(id);
|
self.scopes.collect_garbage(id);
|
||||||
|
|
||||||
if gen_muts {
|
if gen_muts {
|
||||||
self.mutations.remove(id.as_u64());
|
self.mutations.remove(id.as_u64());
|
||||||
@@ -1150,7 +1124,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
}
|
}
|
||||||
VNode::Suspended(s) => {
|
VNode::Suspended(s) => {
|
||||||
let id = s.dom_id.get().unwrap();
|
let id = s.dom_id.get().unwrap();
|
||||||
self.collect_garbage(id);
|
self.scopes.collect_garbage(id);
|
||||||
|
|
||||||
if gen_muts {
|
if gen_muts {
|
||||||
self.mutations.remove(id.as_u64());
|
self.mutations.remove(id.as_u64());
|
||||||
@@ -1158,7 +1132,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
}
|
}
|
||||||
VNode::Anchor(a) => {
|
VNode::Anchor(a) => {
|
||||||
let id = a.dom_id.get().unwrap();
|
let id = a.dom_id.get().unwrap();
|
||||||
self.collect_garbage(id);
|
self.scopes.collect_garbage(id);
|
||||||
|
|
||||||
if gen_muts {
|
if gen_muts {
|
||||||
self.mutations.remove(id.as_u64());
|
self.mutations.remove(id.as_u64());
|
||||||
@@ -1189,7 +1163,7 @@ impl<'bump> DiffState<'bump> {
|
|||||||
self.remove_nodes(Some(root), gen_muts);
|
self.remove_nodes(Some(root), gen_muts);
|
||||||
|
|
||||||
log::debug!("Destroying scope {:?}", scope_id);
|
log::debug!("Destroying scope {:?}", scope_id);
|
||||||
let mut s = self.try_remove(&scope_id).unwrap();
|
let mut s = self.scopes.try_remove(&scope_id).unwrap();
|
||||||
s.hooks.clear_hooks();
|
s.hooks.clear_hooks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ pub(crate) mod nodes;
|
|||||||
pub(crate) mod scope;
|
pub(crate) mod scope;
|
||||||
pub(crate) mod scopearena;
|
pub(crate) mod scopearena;
|
||||||
pub(crate) mod test_dom;
|
pub(crate) mod test_dom;
|
||||||
pub(crate) mod util;
|
|
||||||
pub(crate) mod virtual_dom;
|
pub(crate) mod virtual_dom;
|
||||||
|
|
||||||
pub(crate) mod innerlude {
|
pub(crate) mod innerlude {
|
||||||
@@ -36,7 +35,6 @@ pub(crate) mod innerlude {
|
|||||||
pub use crate::scope::*;
|
pub use crate::scope::*;
|
||||||
pub use crate::scopearena::*;
|
pub use crate::scopearena::*;
|
||||||
pub use crate::test_dom::*;
|
pub use crate::test_dom::*;
|
||||||
pub use crate::util::*;
|
|
||||||
pub use crate::virtual_dom::*;
|
pub use crate::virtual_dom::*;
|
||||||
|
|
||||||
pub type Element = Option<NodeLink>;
|
pub type Element = Option<NodeLink>;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! cheap and *very* fast to construct - building a full tree should be quick.
|
//! cheap and *very* fast to construct - building a full tree should be quick.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
innerlude::{empty_cell, Context, Element, ElementId, Properties, Scope, ScopeId},
|
innerlude::{Context, Element, Properties, Scope, ScopeId},
|
||||||
lazynodes::LazyNodes,
|
lazynodes::LazyNodes,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
@@ -232,6 +232,28 @@ impl Debug for VNode<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An Element's unique identifier.
|
||||||
|
///
|
||||||
|
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
||||||
|
/// unmounted, then the `ElementId` will be reused for a new component.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct ElementId(pub usize);
|
||||||
|
impl std::fmt::Display for ElementId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElementId {
|
||||||
|
pub fn as_u64(self) -> u64 {
|
||||||
|
self.0 as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_cell() -> Cell<Option<ElementId>> {
|
||||||
|
Cell::new(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// A placeholder node only generated when Fragments don't have any children.
|
/// A placeholder node only generated when Fragments don't have any children.
|
||||||
pub struct VAnchor {
|
pub struct VAnchor {
|
||||||
pub dom_id: Cell<Option<ElementId>>,
|
pub dom_id: Cell<Option<ElementId>>,
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
use crate::innerlude::*;
|
|
||||||
use bumpalo::Bump;
|
|
||||||
use std::cell::Cell;
|
|
||||||
|
|
||||||
pub(crate) struct ActiveFrame {
|
|
||||||
pub cur_generation: Cell<usize>,
|
|
||||||
|
|
||||||
// The double-buffering situation that we will use
|
|
||||||
pub frames: [BumpFrame; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveFrame {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let b1 = Bump::new();
|
|
||||||
let b2 = Bump::new();
|
|
||||||
|
|
||||||
let frame_a = BumpFrame {
|
|
||||||
bump: b1,
|
|
||||||
generation: 0.into(),
|
|
||||||
};
|
|
||||||
let frame_b = BumpFrame {
|
|
||||||
bump: b2,
|
|
||||||
generation: 0.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
frames: [frame_a, frame_b],
|
|
||||||
cur_generation: 0.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe fn reset_wip_frame(&self) {
|
|
||||||
// todo: unsafecell or something
|
|
||||||
let bump = self.wip_frame() as *const _ as *mut BumpFrame;
|
|
||||||
let g = &mut *bump;
|
|
||||||
g.bump.reset();
|
|
||||||
|
|
||||||
// self.wip_frame_mut().bump.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The "work in progress frame" represents the frame that is currently being worked on.
|
|
||||||
pub fn wip_frame(&self) -> &BumpFrame {
|
|
||||||
match self.cur_generation.get() & 1 == 0 {
|
|
||||||
true => &self.frames[0],
|
|
||||||
false => &self.frames[1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wip_frame_mut(&mut self) -> &mut BumpFrame {
|
|
||||||
match self.cur_generation.get() & 1 == 0 {
|
|
||||||
true => &mut self.frames[0],
|
|
||||||
false => &mut self.frames[1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The finished frame represents the frame that has been "finished" and cannot be modified again
|
|
||||||
pub fn finished_frame(&self) -> &BumpFrame {
|
|
||||||
match self.cur_generation.get() & 1 == 1 {
|
|
||||||
true => &self.frames[0],
|
|
||||||
false => &self.frames[1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /// Give out our self-referential item with our own borrowed lifetime
|
|
||||||
// pub fn fin_head<'b>(&'b self) -> &'b VNode<'b> {
|
|
||||||
// let cur_head = &self.finished_frame().head_node;
|
|
||||||
// unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Give out our self-referential item with our own borrowed lifetime
|
|
||||||
// pub fn wip_head<'b>(&'b self) -> &'b VNode<'b> {
|
|
||||||
// let cur_head = &self.wip_frame().head_node;
|
|
||||||
// unsafe { std::mem::transmute::<&VNode<'static>, &VNode<'b>>(cur_head) }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn cycle_frame(&mut self) {
|
|
||||||
self.cur_generation.set(self.cur_generation.get() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
//! These tests are bad. I don't have a good way of properly testing the ActiveFrame stuff
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_bump_frame() {
|
|
||||||
let mut frames = ActiveFrame::new();
|
|
||||||
|
|
||||||
// just cycle a few times and make sure we get the right frames out
|
|
||||||
for _ in 0..5 {
|
|
||||||
let fin = frames.finished_frame();
|
|
||||||
let wip = frames.wip_frame();
|
|
||||||
assert_eq!(wip._name, "wip");
|
|
||||||
assert_eq!(fin._name, "fin");
|
|
||||||
frames.cycle_frame();
|
|
||||||
|
|
||||||
let fin = frames.finished_frame();
|
|
||||||
let wip = frames.wip_frame();
|
|
||||||
assert_eq!(wip._name, "fin");
|
|
||||||
assert_eq!(fin._name, "wip");
|
|
||||||
frames.cycle_frame();
|
|
||||||
}
|
|
||||||
assert_eq!(frames.cur_generation.get(), 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
use crate::innerlude::*;
|
|
||||||
|
|
||||||
/// This iterator iterates through a list of virtual children and only returns real children (Elements, Text, Anchors).
|
|
||||||
///
|
|
||||||
/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like
|
|
||||||
/// "InsertBefore".
|
|
||||||
pub(crate) struct RealChildIterator<'a> {
|
|
||||||
scopes: &'a ResourcePool,
|
|
||||||
|
|
||||||
// Heuristically we should never bleed into 4 completely nested fragments/components
|
|
||||||
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
|
|
||||||
// TODO: use const generics instead of the 4 estimation
|
|
||||||
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 4]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> RealChildIterator<'a> {
|
|
||||||
pub fn new(starter: &'a VNode<'a>, scopes: &'a ResourcePool) -> Self {
|
|
||||||
Self {
|
|
||||||
scopes,
|
|
||||||
stack: smallvec::smallvec![(0, starter)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for RealChildIterator<'a> {
|
|
||||||
type Item = &'a VNode<'a>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<&'a VNode<'a>> {
|
|
||||||
let mut should_pop = false;
|
|
||||||
let mut returned_node: Option<&'a VNode<'a>> = None;
|
|
||||||
let mut should_push = None;
|
|
||||||
|
|
||||||
while returned_node.is_none() {
|
|
||||||
if let Some((count, node)) = self.stack.last_mut() {
|
|
||||||
match &node {
|
|
||||||
// We can only exit our looping when we get "real" nodes
|
|
||||||
// This includes fragments and components when they're empty (have a single root)
|
|
||||||
VNode::Element(_) | VNode::Text(_) | VNode::Suspended(_) | VNode::Anchor(_) => {
|
|
||||||
// We've recursed INTO an element/text
|
|
||||||
// We need to recurse *out* of it and move forward to the next
|
|
||||||
should_pop = true;
|
|
||||||
returned_node = Some(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get a fragment we push the next child
|
|
||||||
VNode::Fragment(frag) => {
|
|
||||||
let subcount = *count as usize;
|
|
||||||
|
|
||||||
if frag.children.is_empty() {
|
|
||||||
should_pop = true;
|
|
||||||
returned_node = Some(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if subcount >= frag.children.len() {
|
|
||||||
should_pop = true;
|
|
||||||
} else {
|
|
||||||
should_push = Some(&frag.children[subcount]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For components, we load their root and push them onto the stack
|
|
||||||
VNode::Component(sc) => {
|
|
||||||
let scope = self
|
|
||||||
.scopes
|
|
||||||
.get_scope(&sc.associated_scope.get().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Simply swap the current node on the stack with the root of the component
|
|
||||||
*node = scope.frames.fin_head();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If there's no more items on the stack, we're done!
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if should_pop {
|
|
||||||
self.stack.pop();
|
|
||||||
if let Some((id, _)) = self.stack.last_mut() {
|
|
||||||
*id += 1;
|
|
||||||
}
|
|
||||||
should_pop = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(push) = should_push {
|
|
||||||
self.stack.push((0, push));
|
|
||||||
should_push = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
returned_node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use crate::{innerlude::ScopeState, virtual_dom::VirtualDom, VNode};
|
|
||||||
|
|
||||||
impl std::fmt::Display for VirtualDom {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let base = self.base_scope();
|
|
||||||
let root = base.root_node();
|
|
||||||
|
|
||||||
let renderer = ScopeRenderer {
|
|
||||||
show_fragments: false,
|
|
||||||
skip_components: false,
|
|
||||||
|
|
||||||
_scope: base,
|
|
||||||
_pre_render: false,
|
|
||||||
_newline: true,
|
|
||||||
_indent: true,
|
|
||||||
_max_depth: usize::MAX,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.render(self, root, f, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// render the scope to a string using the rsx! syntax
|
|
||||||
pub(crate) struct ScopeRenderer<'a> {
|
|
||||||
pub skip_components: bool,
|
|
||||||
pub show_fragments: bool,
|
|
||||||
pub _scope: &'a ScopeState,
|
|
||||||
pub _pre_render: bool,
|
|
||||||
pub _newline: bool,
|
|
||||||
pub _indent: bool,
|
|
||||||
pub _max_depth: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is more or less a debug tool, but it'll render the entire tree to the terminal
|
|
||||||
impl<'a> ScopeRenderer<'a> {
|
|
||||||
pub fn render(
|
|
||||||
&self,
|
|
||||||
vdom: &VirtualDom,
|
|
||||||
node: &VNode,
|
|
||||||
f: &mut std::fmt::Formatter,
|
|
||||||
il: u16,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
const INDENT: &str = " ";
|
|
||||||
let write_indent = |_f: &mut std::fmt::Formatter, le| {
|
|
||||||
for _ in 0..le {
|
|
||||||
write!(_f, "{}", INDENT).unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match &node {
|
|
||||||
VNode::Linked(_) => {
|
|
||||||
write!(f, "Linked").unwrap();
|
|
||||||
}
|
|
||||||
VNode::Text(text) => {
|
|
||||||
write_indent(f, il);
|
|
||||||
writeln!(f, "\"{}\"", text.text)?
|
|
||||||
}
|
|
||||||
VNode::Anchor(_anchor) => {
|
|
||||||
write_indent(f, il);
|
|
||||||
writeln!(f, "Anchor {{}}")?;
|
|
||||||
}
|
|
||||||
VNode::Element(el) => {
|
|
||||||
write_indent(f, il);
|
|
||||||
writeln!(f, "{} {{", el.tag_name)?;
|
|
||||||
// write!(f, "element: {}", el.tag_name)?;
|
|
||||||
let mut attr_iter = el.attributes.iter().peekable();
|
|
||||||
|
|
||||||
while let Some(attr) = attr_iter.next() {
|
|
||||||
match attr.namespace {
|
|
||||||
None => {
|
|
||||||
//
|
|
||||||
write_indent(f, il + 1);
|
|
||||||
writeln!(f, "{}: \"{}\"", attr.name, attr.value)?
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(ns) => {
|
|
||||||
// write the opening tag
|
|
||||||
write_indent(f, il + 1);
|
|
||||||
write!(f, " {}:\"", ns)?;
|
|
||||||
let mut cur_ns_el = attr;
|
|
||||||
'ns_parse: loop {
|
|
||||||
write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?;
|
|
||||||
match attr_iter.peek() {
|
|
||||||
Some(next_attr) if next_attr.namespace == Some(ns) => {
|
|
||||||
cur_ns_el = attr_iter.next().unwrap();
|
|
||||||
}
|
|
||||||
_ => break 'ns_parse,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// write the closing tag
|
|
||||||
write!(f, "\"")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in el.children {
|
|
||||||
self.render(vdom, child, f, il + 1)?;
|
|
||||||
}
|
|
||||||
write_indent(f, il);
|
|
||||||
|
|
||||||
writeln!(f, "}}")?;
|
|
||||||
}
|
|
||||||
VNode::Fragment(frag) => {
|
|
||||||
if self.show_fragments {
|
|
||||||
write_indent(f, il);
|
|
||||||
writeln!(f, "Fragment {{")?;
|
|
||||||
for child in frag.children {
|
|
||||||
self.render(vdom, child, f, il + 1)?;
|
|
||||||
}
|
|
||||||
write_indent(f, il);
|
|
||||||
writeln!(f, "}}")?;
|
|
||||||
} else {
|
|
||||||
for child in frag.children {
|
|
||||||
self.render(vdom, child, f, il)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VNode::Component(vcomp) => {
|
|
||||||
let idx = vcomp.associated_scope.get().unwrap();
|
|
||||||
if !self.skip_components {
|
|
||||||
let new_node = vdom.get_scope(&idx).unwrap().root_node();
|
|
||||||
self.render(vdom, new_node, f, il)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VNode::Suspended { .. } => {
|
|
||||||
// we can't do anything with suspended nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//! An event system that's less confusing than Traits + RC;
|
|
||||||
//! This should hopefully make it easier to port to other platforms.
|
|
||||||
//!
|
|
||||||
//! Unfortunately, it is less efficient than the original, but hopefully it's negligible.
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
innerlude::Listener,
|
|
||||||
innerlude::{ElementId, NodeFactory, ScopeId},
|
|
||||||
};
|
|
||||||
use bumpalo::boxed::Box as BumpBox;
|
|
||||||
use std::{
|
|
||||||
any::Any,
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
fmt::Debug,
|
|
||||||
};
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
//! Built-in hooks
|
|
||||||
//!
|
|
||||||
//! This module contains all the low-level built-in hooks that require first party support to work.
|
|
||||||
//!
|
|
||||||
//! Hooks:
|
|
||||||
//! - [`use_hook`]
|
|
||||||
//! - [`use_state_provider`]
|
|
||||||
//! - [`use_state_consumer`]
|
|
||||||
//! - [`use_task`]
|
|
||||||
//! - [`use_suspense`]
|
|
||||||
|
|
||||||
use crate::innerlude::*;
|
|
||||||
use futures_util::FutureExt;
|
|
||||||
use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc};
|
|
||||||
|
|
||||||
// /// Awaits the given task, forcing the component to re-render when the value is ready.
|
|
||||||
// ///
|
|
||||||
// /// Returns the handle to the task and the value (if it is ready, else None).
|
|
||||||
// ///
|
|
||||||
// /// ```
|
|
||||||
// /// static Example: FC<()> = |(cx, props)| {
|
|
||||||
// /// let (task, value) = use_task(|| async {
|
|
||||||
// /// timer::sleep(Duration::from_secs(1)).await;
|
|
||||||
// /// "Hello World"
|
|
||||||
// /// });
|
|
||||||
// ///
|
|
||||||
// /// match contents {
|
|
||||||
// /// Some(contents) => rsx!(cx, div { "{title}" }),
|
|
||||||
// /// None => rsx!(cx, div { "Loading..." }),
|
|
||||||
// /// }
|
|
||||||
// /// };
|
|
||||||
// /// ```
|
|
||||||
// pub fn use_coroutine<'src, Out, Fut, Init>(
|
|
||||||
// cx: Context<'src>,
|
|
||||||
// task_initializer: Init,
|
|
||||||
// ) -> (&'src TaskHandle, &'src Option<Out>)
|
|
||||||
// where
|
|
||||||
// Out: 'static,
|
|
||||||
// Fut: Future<Output = Out> + 'static,
|
|
||||||
// Init: FnOnce() -> Fut + 'src,
|
|
||||||
// {
|
|
||||||
// struct TaskHook<T> {
|
|
||||||
// handle: TaskHandle,
|
|
||||||
// task_dump: Rc<RefCell<Option<T>>>,
|
|
||||||
// value: Option<T>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// todo!()
|
|
||||||
|
|
||||||
// // // whenever the task is complete, save it into th
|
|
||||||
// // cx.use_hook(
|
|
||||||
// // move |_| {
|
|
||||||
// // let task_fut = task_initializer();
|
|
||||||
|
|
||||||
// // let task_dump = Rc::new(RefCell::new(None));
|
|
||||||
|
|
||||||
// // let slot = task_dump.clone();
|
|
||||||
|
|
||||||
// // let updater = cx.schedule_update_any();
|
|
||||||
// // let originator = cx.scope.our_arena_idx;
|
|
||||||
|
|
||||||
// // let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
|
|
||||||
// // *slot.as_ref().borrow_mut() = Some(output);
|
|
||||||
// // updater(originator);
|
|
||||||
// // originator
|
|
||||||
// // })));
|
|
||||||
|
|
||||||
// // TaskHook {
|
|
||||||
// // task_dump,
|
|
||||||
// // value: None,
|
|
||||||
// // handle,
|
|
||||||
// // }
|
|
||||||
// // },
|
|
||||||
// // |hook| {
|
|
||||||
// // if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
|
|
||||||
// // hook.value = Some(val);
|
|
||||||
// // }
|
|
||||||
// // (&hook.handle, &hook.value)
|
|
||||||
// // },
|
|
||||||
// // |_| {},
|
|
||||||
// // )
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Asynchronously render new nodes once the given future has completed.
|
|
||||||
///
|
|
||||||
/// # Easda
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
///
|
|
||||||
pub fn use_suspense<'src, Out, Fut, Cb>(
|
|
||||||
cx: Context<'src>,
|
|
||||||
task_initializer: impl FnOnce() -> Fut,
|
|
||||||
user_callback: Cb,
|
|
||||||
) -> Element
|
|
||||||
where
|
|
||||||
Fut: Future<Output = Out>,
|
|
||||||
Out: 'static,
|
|
||||||
Cb: FnMut(&Out) -> Element + 'src,
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
General strategy:
|
|
||||||
- Create a slot for the future to dump its output into
|
|
||||||
- Create a new future feeding off the user's future that feeds the output into that slot
|
|
||||||
- Submit that future as a task
|
|
||||||
- Take the task handle id and attach that to our suspended node
|
|
||||||
- when the hook runs, check if the value exists
|
|
||||||
- if it does, then we can render the node directly
|
|
||||||
- if it doesn't, then we render a suspended node along with with the callback and task id
|
|
||||||
*/
|
|
||||||
todo!()
|
|
||||||
// cx.use_hook(
|
|
||||||
// move |_| {
|
|
||||||
// let value = Rc::new(RefCell::new(None));
|
|
||||||
// let slot = value.clone();
|
|
||||||
// let originator = cx.scope.our_arena_idx;
|
|
||||||
|
|
||||||
// let handle = cx.submit_task(Box::pin(task_initializer().then(
|
|
||||||
// move |output| async move {
|
|
||||||
// *slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
|
|
||||||
// originator
|
|
||||||
// },
|
|
||||||
// )));
|
|
||||||
|
|
||||||
// SuspenseHook { handle, value }
|
|
||||||
// },
|
|
||||||
// move |hook| {
|
|
||||||
// // If the value exists, just run the callback to get the contents
|
|
||||||
// // if the value doesn't exist, we want to render a suspended node with an associated callback
|
|
||||||
// if let Some(value) = hook.value.borrow().as_ref() {
|
|
||||||
// let out = value.downcast_ref::<Out>().unwrap();
|
|
||||||
// user_callback(out)
|
|
||||||
// } else {
|
|
||||||
// let value = hook.value.clone();
|
|
||||||
|
|
||||||
// let id = hook.handle.our_id;
|
|
||||||
|
|
||||||
// let bump = cx.bump();
|
|
||||||
|
|
||||||
// use bumpalo::boxed::Box as BumpBox;
|
|
||||||
|
|
||||||
// let f: &mut dyn FnMut() -> Element<'src> = bump.alloc(move || {
|
|
||||||
// let val = value.borrow();
|
|
||||||
|
|
||||||
// let out = val
|
|
||||||
// .as_ref()
|
|
||||||
// .unwrap()
|
|
||||||
// .as_ref()
|
|
||||||
// .downcast_ref::<Out>()
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
// user_callback(out)
|
|
||||||
// });
|
|
||||||
// let callback = unsafe { BumpBox::from_raw(f) };
|
|
||||||
|
|
||||||
// Some(VNode::Suspended(bump.alloc(VSuspended {
|
|
||||||
// dom_id: empty_cell(),
|
|
||||||
// task_id: id,
|
|
||||||
// callback: RefCell::new(Some(callback)),
|
|
||||||
// })))
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// |_| {},
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub(crate) struct SuspenseHook {
|
|
||||||
// pub handle: TaskHandle,
|
|
||||||
// pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct NodeRef<'src, T: 'static>(&'src RefCell<Option<T>>);
|
|
||||||
|
|
||||||
impl<'a, T> Deref for NodeRef<'a, T> {
|
|
||||||
type Target = RefCell<Option<T>>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn use_node_ref<T, P>(cx: Context) -> NodeRef<T> {
|
|
||||||
cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f })
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
// let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
|
|
||||||
|
|
||||||
// let mut garbage_list = scope.consume_garbage();
|
|
||||||
|
|
||||||
// let mut scopes_to_kill = Vec::new();
|
|
||||||
// while let Some(node) = garbage_list.pop() {
|
|
||||||
// match &node.kind {
|
|
||||||
// VNodeKind::Text(_) => {
|
|
||||||
// self.shared.collect_garbage(node.direct_id());
|
|
||||||
// }
|
|
||||||
// VNodeKind::Anchor(_) => {
|
|
||||||
// self.shared.collect_garbage(node.direct_id());
|
|
||||||
// }
|
|
||||||
// VNodeKind::Suspended(_) => {
|
|
||||||
// self.shared.collect_garbage(node.direct_id());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// VNodeKind::Element(el) => {
|
|
||||||
// self.shared.collect_garbage(node.direct_id());
|
|
||||||
// for child in el.children {
|
|
||||||
// garbage_list.push(child);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// VNodeKind::Fragment(frag) => {
|
|
||||||
// for child in frag.children {
|
|
||||||
// garbage_list.push(child);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// VNodeKind::Component(comp) => {
|
|
||||||
// // TODO: run the hook destructors and then even delete the scope
|
|
||||||
|
|
||||||
// let scope_id = comp.ass_scope.get().unwrap();
|
|
||||||
// let scope = self.get_scope(scope_id).unwrap();
|
|
||||||
// let root = scope.root();
|
|
||||||
// garbage_list.push(root);
|
|
||||||
// scopes_to_kill.push(scope_id);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for scope in scopes_to_kill {
|
|
||||||
// // oy kill em
|
|
||||||
// log::debug!("should be removing scope {:#?}", scope);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // On the primary event queue, there is no batching, we take them off one-by-one
|
|
||||||
// let trigger = match receiver.try_next() {
|
|
||||||
// Ok(Some(trigger)) => trigger,
|
|
||||||
// _ => {
|
|
||||||
// // Continuously poll the future pool and the event receiver for work
|
|
||||||
// let mut tasks = self.shared.async_tasks.borrow_mut();
|
|
||||||
// let tasks_tasks = tasks.next();
|
|
||||||
|
|
||||||
// // if the new event generates work more important than our current fiber, we should consider switching
|
|
||||||
// // only switch if it impacts different scopes.
|
|
||||||
// let mut ui_receiver = self.shared.ui_event_receiver.borrow_mut();
|
|
||||||
// let ui_reciv_task = ui_receiver.next();
|
|
||||||
|
|
||||||
// // right now, this polling method will only catch batched set_states that don't get awaited.
|
|
||||||
// // However, in the future, we might be interested in batching set_states across await points
|
|
||||||
// let immediate_tasks = ();
|
|
||||||
|
|
||||||
// futures_util::pin_mut!(tasks_tasks);
|
|
||||||
// futures_util::pin_mut!(ui_reciv_task);
|
|
||||||
|
|
||||||
// // Poll the event receiver and the future pool for work
|
|
||||||
// // Abort early if our deadline has ran out
|
|
||||||
// let mut deadline = (&mut deadline_future).fuse();
|
|
||||||
|
|
||||||
// let trig = futures_util::select! {
|
|
||||||
// trigger = tasks_tasks => trigger,
|
|
||||||
// trigger = ui_reciv_task => trigger,
|
|
||||||
|
|
||||||
// // abort if we're out of time
|
|
||||||
// _ = deadline => { return Ok(diff_machine.mutations); }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// trig.unwrap()
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// async fn select_next_event(&mut self) -> Option<EventTrigger> {
|
|
||||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
|
||||||
|
|
||||||
// // drain the in-flight events so that we can sort them out with the current events
|
|
||||||
// while let Ok(Some(trigger)) = receiver.try_next() {
|
|
||||||
// log::info!("retrieving event from receiver");
|
|
||||||
// let key = self.shared.make_trigger_key(&trigger);
|
|
||||||
// self.pending_events.insert(key, trigger);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if self.pending_events.is_empty() {
|
|
||||||
// // Continuously poll the future pool and the event receiver for work
|
|
||||||
// let mut tasks = self.shared.async_tasks.borrow_mut();
|
|
||||||
// let tasks_tasks = tasks.next();
|
|
||||||
|
|
||||||
// let mut receiver = self.shared.task_receiver.borrow_mut();
|
|
||||||
// let reciv_task = receiver.next();
|
|
||||||
|
|
||||||
// futures_util::pin_mut!(tasks_tasks);
|
|
||||||
// futures_util::pin_mut!(reciv_task);
|
|
||||||
|
|
||||||
// let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await {
|
|
||||||
// futures_util::future::Either::Left((trigger, _)) => trigger,
|
|
||||||
// futures_util::future::Either::Right((trigger, _)) => trigger,
|
|
||||||
// }
|
|
||||||
// .unwrap();
|
|
||||||
// let key = self.shared.make_trigger_key(&trigger);
|
|
||||||
// self.pending_events.insert(key, trigger);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // pop the most important event off
|
|
||||||
// let key = self.pending_events.keys().next().unwrap().clone();
|
|
||||||
// let trigger = self.pending_events.remove(&key).unwrap();
|
|
||||||
|
|
||||||
// Some(trigger)
|
|
||||||
// }
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use crate::innerlude::*;
|
|
||||||
use slab::Slab;
|
|
||||||
|
|
||||||
use std::{cell::UnsafeCell, rc::Rc};
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct ResourcePool {
|
|
||||||
/*
|
|
||||||
This *has* to be an UnsafeCell.
|
|
||||||
|
|
||||||
Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to
|
|
||||||
its bumpframe contents immutably.
|
|
||||||
|
|
||||||
However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly
|
|
||||||
simple to audit.
|
|
||||||
|
|
||||||
Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!)
|
|
||||||
*/
|
|
||||||
pub components: Rc<UnsafeCell<Slab<ScopeInner>>>,
|
|
||||||
|
|
||||||
/*
|
|
||||||
Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy
|
|
||||||
that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client.
|
|
||||||
|
|
||||||
This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse.
|
|
||||||
|
|
||||||
In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes...
|
|
||||||
*/
|
|
||||||
pub raw_elements: Rc<UnsafeCell<Slab<*const VNode<'static>>>>,
|
|
||||||
|
|
||||||
pub channel: EventChannel,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourcePool {
|
|
||||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
|
||||||
pub fn get_scope(&self, idx: &ScopeId) -> Option<&ScopeInner> {
|
|
||||||
let inner = unsafe { &*self.components.get() };
|
|
||||||
inner.get(idx.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// this is unsafe because the caller needs to track which other scopes it's already using
|
|
||||||
pub fn get_scope_mut(&self, idx: &ScopeId) -> Option<&mut ScopeInner> {
|
|
||||||
let inner = unsafe { &mut *self.components.get() };
|
|
||||||
inner.get_mut(idx.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_remove(&self, id: &ScopeId) -> Option<ScopeInner> {
|
|
||||||
let inner = unsafe { &mut *self.components.get() };
|
|
||||||
Some(inner.remove(id.0))
|
|
||||||
// .try_remove(id.0)
|
|
||||||
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId {
|
|
||||||
let els = unsafe { &mut *self.raw_elements.get() };
|
|
||||||
let entry = els.vacant_entry();
|
|
||||||
let key = entry.key();
|
|
||||||
let id = ElementId(key);
|
|
||||||
let node = node as *const _;
|
|
||||||
let node = unsafe { std::mem::transmute(node) };
|
|
||||||
entry.insert(node);
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return the id, freeing the space of the original node
|
|
||||||
pub fn collect_garbage(&self, id: ElementId) {
|
|
||||||
let els = unsafe { &mut *self.raw_elements.get() };
|
|
||||||
els.remove(id.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> ScopeInner) -> ScopeId {
|
|
||||||
let g = unsafe { &mut *self.components.get() };
|
|
||||||
let entry = g.vacant_entry();
|
|
||||||
let id = ScopeId(entry.key());
|
|
||||||
entry.insert(f(id));
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
/*
|
|
||||||
Welcome to Dioxus's cooperative, priority-based scheduler.
|
|
||||||
|
|
||||||
I hope you enjoy your stay.
|
|
||||||
|
|
||||||
Some essential reading:
|
|
||||||
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L197-L200
|
|
||||||
- https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js#L440
|
|
||||||
- https://github.com/WICG/is-input-pending
|
|
||||||
- https://web.dev/rail/
|
|
||||||
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
|
|
||||||
|
|
||||||
# What's going on?
|
|
||||||
|
|
||||||
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
|
|
||||||
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
|
|
||||||
point in being "fast" if you can't also be "responsive."
|
|
||||||
|
|
||||||
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
|
|
||||||
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
|
|
||||||
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
|
|
||||||
different types of work simultaneously.
|
|
||||||
|
|
||||||
# How does it work?
|
|
||||||
|
|
||||||
Per the RAIL guide, we want to make sure that A) inputs are handled ASAP and B) animations are not blocked.
|
|
||||||
React-three-fiber is a testament to how amazing this can be - a ThreeJS scene is threaded in between work periods of
|
|
||||||
React, and the UI still stays snappy!
|
|
||||||
|
|
||||||
While it's straightforward to run code ASAP and be as "fast as possible", what's not _not_ straightforward is how to do
|
|
||||||
this while not blocking the main thread. The current prevailing thought is to stop working periodically so the browser
|
|
||||||
has time to paint and run animations. When the browser is finished, we can step in and continue our work.
|
|
||||||
|
|
||||||
React-Fiber uses the "Fiber" concept to achieve a pause-resume functionality. This is worth reading up on, but not
|
|
||||||
necessary to understand what we're doing here. In Dioxus, our DiffMachine is guided by DiffInstructions - essentially
|
|
||||||
"commands" that guide the Diffing algorithm through the tree. Our "diff_scope" method is async - we can literally pause
|
|
||||||
our DiffMachine "mid-sentence" (so to speak) by just stopping the poll on the future. The DiffMachine periodically yields
|
|
||||||
so Rust's async machinery can take over, allowing us to customize when exactly to pause it.
|
|
||||||
|
|
||||||
React's "should_yield" method is more complex than ours, and I assume we'll move in that direction as Dioxus matures. For
|
|
||||||
now, Dioxus just assumes a TimeoutFuture, and selects! on both the Diff algorithm and timeout. If the DiffMachine finishes
|
|
||||||
before the timeout, then Dioxus will work on any pending work in the interim. If there is no pending work, then the changes
|
|
||||||
are committed, and coroutines are polled during the idle period. However, if the timeout expires, then the DiffMachine
|
|
||||||
future is paused and saved (self-referentially).
|
|
||||||
|
|
||||||
# Priority System
|
|
||||||
|
|
||||||
So far, we've been able to thread our Dioxus work between animation frames - the main thread is not blocked! But that
|
|
||||||
doesn't help us _under load_. How do we still stay snappy... even if we're doing a lot of work? Well, that's where
|
|
||||||
priorities come into play. The goal with priorities is to schedule shorter work as a "high" priority and longer work as
|
|
||||||
a "lower" priority. That way, we can interrupt long-running low-priority work with short-running high-priority work.
|
|
||||||
|
|
||||||
React's priority system is quite complex.
|
|
||||||
|
|
||||||
There are 5 levels of priority and 2 distinctions between UI events (discrete, continuous). I believe React really only
|
|
||||||
uses 3 priority levels and "idle" priority isn't used... Regardless, there's some batching going on.
|
|
||||||
|
|
||||||
For Dioxus, we're going with a 4 tier priority system:
|
|
||||||
- Sync: Things that need to be done by the next frame, like TextInput on controlled elements
|
|
||||||
- High: for events that block all others - clicks, keyboard, and hovers
|
|
||||||
- Medium: for UI events caused by the user but not directly - scrolls/forms/focus (all other events)
|
|
||||||
- Low: set_state called asynchronously, and anything generated by suspense
|
|
||||||
|
|
||||||
In "Sync" state, we abort our "idle wait" future, and resolve the sync queue immediately and escape. Because we completed
|
|
||||||
work before the next rAF, any edits can be immediately processed before the frame ends. Generally though, we want to leave
|
|
||||||
as much time to rAF as possible. "Sync" is currently only used by onInput - we'll leave some docs telling people not to
|
|
||||||
do anything too arduous from onInput.
|
|
||||||
|
|
||||||
For the rest, we defer to the rIC period and work down each queue from high to low.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::innerlude::*;
|
|
||||||
use bumpalo::Bump;
|
|
||||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
|
||||||
use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt};
|
|
||||||
use fxhash::FxHashMap;
|
|
||||||
use fxhash::FxHashSet;
|
|
||||||
use indexmap::IndexSet;
|
|
||||||
use slab::Slab;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::Poll;
|
|
||||||
use std::{
|
|
||||||
any::{Any, TypeId},
|
|
||||||
cell::{Cell, UnsafeCell},
|
|
||||||
collections::{HashSet, VecDeque},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct EventChannel {
|
|
||||||
pub sender: UnboundedSender<SchedulerMsg>,
|
|
||||||
pub get_shared_context: GetSharedContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type GetSharedContext = Rc<dyn Fn(ScopeId, TypeId) -> Option<Rc<dyn Any>>>;
|
|
||||||
|
|
||||||
pub enum SchedulerMsg {
|
|
||||||
// events from the host
|
|
||||||
UiEvent(UserEvent),
|
|
||||||
|
|
||||||
// setstate
|
|
||||||
Immediate(ScopeId),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scheduler holds basically everything around "working"
|
|
||||||
///
|
|
||||||
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
|
|
||||||
///
|
|
||||||
/// In Dioxus, the scheduler provides 4 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
|
|
||||||
/// out.
|
|
||||||
///
|
|
||||||
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
|
|
||||||
/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority
|
|
||||||
/// task shift to a higher priority task that needs mutable access to the same scopes.
|
|
||||||
///
|
|
||||||
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
|
|
||||||
///
|
|
||||||
/// There's a lot of raw pointers here...
|
|
||||||
///
|
|
||||||
/// Since we're building self-referential structures for each component, we need to make sure that the referencs stay stable
|
|
||||||
/// The best way to do that is a bump allocator.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
pub(crate) struct Scheduler {
|
|
||||||
// /// All mounted components are arena allocated to make additions, removals, and references easy to work with
|
|
||||||
// /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
|
||||||
// ///
|
|
||||||
// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas
|
|
||||||
// /// and rusts's guarantees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
|
|
||||||
// pub pool: ResourcePool,
|
|
||||||
//
|
|
||||||
pub component_arena: Bump,
|
|
||||||
|
|
||||||
pub free_components: VecDeque<*mut ScopeInner>,
|
|
||||||
|
|
||||||
pub heuristics: FxHashMap<FcSlot, Heuristic>,
|
|
||||||
|
|
||||||
pub receiver: UnboundedReceiver<SchedulerMsg>,
|
|
||||||
|
|
||||||
// Garbage stored
|
|
||||||
pub pending_garbage: FxHashSet<ScopeId>,
|
|
||||||
|
|
||||||
// Every component that has futures that need to be polled
|
|
||||||
pub pending_futures: FxHashSet<ScopeId>,
|
|
||||||
|
|
||||||
// // scheduler stuff
|
|
||||||
// pub current_priority: EventPriority,
|
|
||||||
pub ui_events: VecDeque<UserEvent>,
|
|
||||||
|
|
||||||
pub pending_immediates: VecDeque<ScopeId>,
|
|
||||||
|
|
||||||
pub batched_events: VecDeque<UserEvent>,
|
|
||||||
|
|
||||||
pub garbage_scopes: HashSet<ScopeId>,
|
|
||||||
|
|
||||||
pub dirty_scopes: IndexSet<ScopeId>,
|
|
||||||
|
|
||||||
pub saved_state: Option<SavedDiffWork<'static>>,
|
|
||||||
|
|
||||||
pub in_progress: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type FcSlot = *const ();
|
|
||||||
|
|
||||||
pub struct Heuristic {
|
|
||||||
hook_arena_size: usize,
|
|
||||||
node_arena_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scheduler {
|
|
||||||
pub(crate) fn new(
|
|
||||||
sender: UnboundedSender<SchedulerMsg>,
|
|
||||||
receiver: UnboundedReceiver<SchedulerMsg>,
|
|
||||||
component_capacity: usize,
|
|
||||||
element_capacity: usize,
|
|
||||||
) -> Self {
|
|
||||||
/*
|
|
||||||
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
|
|
||||||
Perhaps this should be configurable from some external config?
|
|
||||||
*/
|
|
||||||
|
|
||||||
// let components = Rc::new(UnsafeCell::new(Slab::with_capacity(component_capacity)));
|
|
||||||
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(element_capacity)));
|
|
||||||
|
|
||||||
let channel = EventChannel {
|
|
||||||
sender: sender.clone(),
|
|
||||||
get_shared_context: {
|
|
||||||
todo!()
|
|
||||||
// let components = components.clone();
|
|
||||||
// Rc::new(move |id, ty| {
|
|
||||||
// let components = unsafe { &*components.get() };
|
|
||||||
// let mut search: Option<&ScopeInner> = components.get(id.0);
|
|
||||||
// while let Some(inner) = search.take() {
|
|
||||||
// if let Some(shared) = inner.shared_contexts.borrow().get(&ty) {
|
|
||||||
// return Some(shared.clone());
|
|
||||||
// } else {
|
|
||||||
// search = inner.parent_idx.map(|id| components.get(id.0)).flatten();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// None
|
|
||||||
// })
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// let pool = ResourcePool {
|
|
||||||
// components,
|
|
||||||
// raw_elements,
|
|
||||||
// channel,
|
|
||||||
// };
|
|
||||||
|
|
||||||
let saved_state = SavedDiffWork {
|
|
||||||
mutations: Mutations::new(),
|
|
||||||
stack: DiffStack::new(),
|
|
||||||
seen_scopes: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
// pool,
|
|
||||||
receiver,
|
|
||||||
|
|
||||||
pending_garbage: FxHashSet::default(),
|
|
||||||
|
|
||||||
ui_events: VecDeque::new(),
|
|
||||||
|
|
||||||
pending_immediates: VecDeque::new(),
|
|
||||||
|
|
||||||
batched_events: VecDeque::new(),
|
|
||||||
|
|
||||||
garbage_scopes: HashSet::new(),
|
|
||||||
|
|
||||||
pending_futures: Default::default(),
|
|
||||||
dirty_scopes: Default::default(),
|
|
||||||
saved_state: Some(saved_state),
|
|
||||||
in_progress: false,
|
|
||||||
|
|
||||||
heuristics: todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the event is discrete
|
|
||||||
pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
|
|
||||||
let (discrete, priority) = event_meta(&event);
|
|
||||||
|
|
||||||
if let Some(scope) = self.get_scope_mut(&event.scope) {
|
|
||||||
if let Some(element) = event.mounted_dom_id {
|
|
||||||
// TODO: bubble properly here
|
|
||||||
scope.call_listener(event, element);
|
|
||||||
|
|
||||||
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
|
|
||||||
//
|
|
||||||
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use EventPriority::*;
|
|
||||||
|
|
||||||
// match priority {
|
|
||||||
// Immediate => todo!(),
|
|
||||||
// High => todo!(),
|
|
||||||
// Medium => todo!(),
|
|
||||||
// Low => todo!(),
|
|
||||||
// }
|
|
||||||
|
|
||||||
discrete
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_work(&mut self) {
|
|
||||||
// while let Some(trigger) = self.ui_events.pop_back() {
|
|
||||||
// if let Some(scope) = self.get_scope_mut(&trigger.scope) {}
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// nothing to do, no events on channels, no work
|
|
||||||
pub fn has_any_work(&self) -> bool {
|
|
||||||
!(self.dirty_scopes.is_empty() && self.ui_events.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
|
|
||||||
fn balance_lanes(&mut self) {}
|
|
||||||
|
|
||||||
fn save_work(&mut self, lane: SavedDiffWork) {
|
|
||||||
let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) };
|
|
||||||
self.saved_state = Some(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn load_work(&mut self) -> SavedDiffWork<'static> {
|
|
||||||
self.saved_state.take().unwrap().extend()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
|
||||||
match msg {
|
|
||||||
SchedulerMsg::Immediate(_) => todo!(),
|
|
||||||
|
|
||||||
SchedulerMsg::UiEvent(event) => {
|
|
||||||
//
|
|
||||||
|
|
||||||
let (discrete, priority) = event_meta(&event);
|
|
||||||
|
|
||||||
if let Some(scope) = self.get_scope_mut(&event.scope) {
|
|
||||||
if let Some(element) = event.mounted_dom_id {
|
|
||||||
// TODO: bubble properly here
|
|
||||||
scope.call_listener(event, element);
|
|
||||||
|
|
||||||
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
|
|
||||||
//
|
|
||||||
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
discrete;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
|
||||||
///
|
|
||||||
/// Returns true if the lane is finished before the deadline could be met.
|
|
||||||
pub fn work_on_current_lane(
|
|
||||||
&mut self,
|
|
||||||
deadline_reached: impl FnMut() -> bool,
|
|
||||||
mutations: &mut Vec<Mutations>,
|
|
||||||
) -> bool {
|
|
||||||
// Work through the current subtree, and commit the results when it finishes
|
|
||||||
// When the deadline expires, give back the work
|
|
||||||
let saved_state = unsafe { self.load_work() };
|
|
||||||
|
|
||||||
// We have to split away some parts of ourself - current lane is borrowed mutably
|
|
||||||
let shared = self.clone();
|
|
||||||
let mut machine = unsafe { saved_state.promote(&shared) };
|
|
||||||
|
|
||||||
let mut ran_scopes = FxHashSet::default();
|
|
||||||
|
|
||||||
if machine.stack.is_empty() {
|
|
||||||
let shared = self.clone();
|
|
||||||
|
|
||||||
self.dirty_scopes
|
|
||||||
.retain(|id| shared.get_scope(id).is_some());
|
|
||||||
self.dirty_scopes.sort_by(|a, b| {
|
|
||||||
let h1 = shared.get_scope(a).unwrap().height;
|
|
||||||
let h2 = shared.get_scope(b).unwrap().height;
|
|
||||||
h1.cmp(&h2).reverse()
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(scopeid) = self.dirty_scopes.pop() {
|
|
||||||
log::info!("handling dirty scope {:?}", scopeid);
|
|
||||||
if !ran_scopes.contains(&scopeid) {
|
|
||||||
ran_scopes.insert(scopeid);
|
|
||||||
log::debug!("about to run scope {:?}", scopeid);
|
|
||||||
|
|
||||||
if let Some(component) = self.get_scope_mut(&scopeid) {
|
|
||||||
if component.run_scope(&self) {
|
|
||||||
let (old, new) =
|
|
||||||
(component.frames.wip_head(), component.frames.fin_head());
|
|
||||||
// let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
|
|
||||||
machine.stack.scope_stack.push(scopeid);
|
|
||||||
machine.stack.push(DiffInstruction::Diff { new, old });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let work_completed = machine.work(deadline_reached);
|
|
||||||
|
|
||||||
// log::debug!("raw edits {:?}", machine.mutations.edits);
|
|
||||||
|
|
||||||
let mut machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
|
|
||||||
// let mut saved = machine.save();
|
|
||||||
|
|
||||||
if work_completed {
|
|
||||||
for node in machine.seen_scopes.drain() {
|
|
||||||
// self.dirty_scopes.clear();
|
|
||||||
// self.ui_events.clear();
|
|
||||||
self.dirty_scopes.remove(&node);
|
|
||||||
// self.dirty_scopes.remove(&node);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_mutations = Mutations::new();
|
|
||||||
|
|
||||||
for edit in machine.mutations.edits.drain(..) {
|
|
||||||
new_mutations.edits.push(edit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for edit in saved.edits.drain(..) {
|
|
||||||
// new_mutations.edits.push(edit);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// std::mem::swap(&mut new_mutations, &mut saved.mutations);
|
|
||||||
|
|
||||||
mutations.push(new_mutations);
|
|
||||||
|
|
||||||
// log::debug!("saved edits {:?}", mutations);
|
|
||||||
|
|
||||||
let mut saved = machine.save();
|
|
||||||
self.save_work(saved);
|
|
||||||
true
|
|
||||||
|
|
||||||
// self.save_work(saved);
|
|
||||||
// false
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The primary workhorse of the VirtualDOM.
|
|
||||||
///
|
|
||||||
/// Uses some fairly complex logic to schedule what work should be produced.
|
|
||||||
///
|
|
||||||
/// Returns a list of successful mutations.
|
|
||||||
pub fn work_with_deadline<'a>(
|
|
||||||
&'a mut self,
|
|
||||||
mut deadline: impl FnMut() -> bool,
|
|
||||||
) -> Vec<Mutations<'a>> {
|
|
||||||
/*
|
|
||||||
Strategy:
|
|
||||||
- When called, check for any UI events that might've been received since the last frame.
|
|
||||||
- Dump all UI events into a "pending discrete" queue and a "pending continuous" queue.
|
|
||||||
|
|
||||||
- If there are any pending discrete events, then elevate our priority level. If our priority level is already "high,"
|
|
||||||
then we need to finish the high priority work first. If the current work is "low" then analyze what scopes
|
|
||||||
will be invalidated by this new work. If this interferes with any in-flight medium or low work, then we need
|
|
||||||
to bump the other work out of the way, or choose to process it so we don't have any conflicts.
|
|
||||||
'static components have a leg up here since their work can be re-used among multiple scopes.
|
|
||||||
"High priority" is only for blocking! Should only be used on "clicks"
|
|
||||||
|
|
||||||
- If there are no pending discrete events, then check for continuous events. These can be completely batched
|
|
||||||
|
|
||||||
- we batch completely until we run into a discrete event
|
|
||||||
- all continuous events are batched together
|
|
||||||
- so D C C C C C would be two separate events - D and C. IE onclick and onscroll
|
|
||||||
- D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
|
|
||||||
|
|
||||||
- !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
|
|
||||||
- we need to keep track of element parents to be able to traverse properly
|
|
||||||
|
|
||||||
|
|
||||||
Open questions:
|
|
||||||
- what if we get two clicks from the component during the same slice?
|
|
||||||
- should we batch?
|
|
||||||
- react says no - they are continuous
|
|
||||||
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
|
||||||
*/
|
|
||||||
let mut committed_mutations = Vec::<Mutations<'static>>::new();
|
|
||||||
|
|
||||||
while self.has_any_work() {
|
|
||||||
while let Ok(Some(msg)) = self.receiver.try_next() {
|
|
||||||
match msg {
|
|
||||||
SchedulerMsg::Immediate(im) => {
|
|
||||||
self.dirty_scopes.insert(im);
|
|
||||||
}
|
|
||||||
SchedulerMsg::UiEvent(evt) => {
|
|
||||||
self.ui_events.push_back(evt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// switch our priority, pop off any work
|
|
||||||
while let Some(event) = self.ui_events.pop_front() {
|
|
||||||
if let Some(scope) = self.get_scope_mut(&event.scope) {
|
|
||||||
if let Some(element) = event.mounted_dom_id {
|
|
||||||
log::info!("Calling listener {:?}, {:?}", event.scope, element);
|
|
||||||
|
|
||||||
// TODO: bubble properly here
|
|
||||||
scope.call_listener(event, element);
|
|
||||||
|
|
||||||
while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
|
|
||||||
match dirty_scope {
|
|
||||||
SchedulerMsg::Immediate(im) => {
|
|
||||||
self.dirty_scopes.insert(im);
|
|
||||||
}
|
|
||||||
SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations);
|
|
||||||
|
|
||||||
if !work_complete {
|
|
||||||
return committed_mutations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
committed_mutations
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Work the scheduler down, not polling any ongoing tasks.
|
|
||||||
///
|
|
||||||
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
|
||||||
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
|
||||||
let mut committed_mutations = Vec::new();
|
|
||||||
|
|
||||||
while let Ok(Some(msg)) = self.receiver.try_next() {
|
|
||||||
self.handle_channel_msg(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_any_work() {
|
|
||||||
return committed_mutations;
|
|
||||||
}
|
|
||||||
|
|
||||||
while self.has_any_work() {
|
|
||||||
self.prepare_work();
|
|
||||||
self.work_on_current_lane(|| false, &mut committed_mutations);
|
|
||||||
}
|
|
||||||
|
|
||||||
committed_mutations
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restart the entire VirtualDOM from scratch, wiping away any old state and components.
|
|
||||||
///
|
|
||||||
/// Typically used to kickstart the VirtualDOM after initialization.
|
|
||||||
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
|
|
||||||
let mut shared = self.clone();
|
|
||||||
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
|
|
||||||
|
|
||||||
// TODO: drain any in-flight work
|
|
||||||
let cur_component = self
|
|
||||||
.pool
|
|
||||||
.get_scope_mut(&base_scope)
|
|
||||||
.expect("The base scope should never be moved");
|
|
||||||
|
|
||||||
log::debug!("rebuild {:?}", base_scope);
|
|
||||||
|
|
||||||
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
|
|
||||||
if cur_component.run_scope(&self) {
|
|
||||||
diff_machine
|
|
||||||
.stack
|
|
||||||
.create_node(cur_component.frames.fin_head(), MountType::Append);
|
|
||||||
|
|
||||||
diff_machine.stack.scope_stack.push(base_scope);
|
|
||||||
|
|
||||||
diff_machine.work(|| false);
|
|
||||||
} else {
|
|
||||||
// todo: should this be a hard error?
|
|
||||||
log::warn!(
|
|
||||||
"Component failed to run successfully during rebuild.
|
|
||||||
This does not result in a failed rebuild, but indicates a logic failure within your app."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe { std::mem::transmute(diff_machine.mutations) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations {
|
|
||||||
let cur_component = self
|
|
||||||
.pool
|
|
||||||
.get_scope_mut(&base_scope)
|
|
||||||
.expect("The base scope should never be moved");
|
|
||||||
|
|
||||||
log::debug!("hard diff {:?}", base_scope);
|
|
||||||
|
|
||||||
if cur_component.run_scope(&self) {
|
|
||||||
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self);
|
|
||||||
diff_machine.cfg.force_diff = true;
|
|
||||||
diff_machine.diff_scope(base_scope);
|
|
||||||
diff_machine.mutations
|
|
||||||
} else {
|
|
||||||
Mutations::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Future for Scheduler {
|
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let mut all_pending = true;
|
|
||||||
|
|
||||||
for fut in self.pending_futures.iter() {
|
|
||||||
let scope = self
|
|
||||||
.pool
|
|
||||||
.get_scope_mut(&fut)
|
|
||||||
.expect("Scope should never be moved");
|
|
||||||
|
|
||||||
let items = scope.items.get_mut();
|
|
||||||
for task in items.tasks.iter_mut() {
|
|
||||||
let t = task.as_mut();
|
|
||||||
let g = unsafe { Pin::new_unchecked(t) };
|
|
||||||
match g.poll(cx) {
|
|
||||||
Poll::Ready(r) => {
|
|
||||||
all_pending = false;
|
|
||||||
}
|
|
||||||
Poll::Pending => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match all_pending {
|
|
||||||
true => Poll::Pending,
|
|
||||||
false => Poll::Ready(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
//! A threadsafe wrapper for the VirtualDom
|
|
||||||
//!
|
|
||||||
//! This is an experimental module, and must be explicitly opted-into.
|
|
||||||
//!
|
|
||||||
//! It's not guaranteed that this module produces safe results, so use at your own peril.
|
|
||||||
//!
|
|
||||||
//! The only real "right" answer to a Send VirtualDom is by ensuring all hook data is Send
|
|
||||||
//!
|
|
||||||
//!
|
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
|
||||||
|
|
||||||
use crate::VirtualDom;
|
|
||||||
|
|
||||||
/// A threadsafe wrapper for the Dioxus VirtualDom.
|
|
||||||
///
|
|
||||||
/// The Dioxus VirtualDom is not normally `Send` because user code can contain non-`Send` types. However, it is important
|
|
||||||
/// to have a VirtualDom that is `Send` when used in server-side code since very few web frameworks support non-send
|
|
||||||
/// handlers.
|
|
||||||
///
|
|
||||||
/// To address this, we have the `ThreadsafeVirtualDom` type which is a threadsafe wrapper for the VirtualDom. To access
|
|
||||||
/// the VirtualDom, it must be first unlocked using the `lock` method. This locks the VirtualDom through a mutex and
|
|
||||||
/// prevents any user code from leaking out. It is not possible to acquire any non-`Send` types from inside the VirtualDom.
|
|
||||||
///
|
|
||||||
/// The only way data may be accessed through the VirtualDom is from the "root props" method or by accessing a `Scope`
|
|
||||||
/// directly. Even then, it's not possible to access any hook data. This means that non-Send types are only "in play"
|
|
||||||
/// while the VirtualDom is locked with a non-Send marker.
|
|
||||||
///
|
|
||||||
/// Calling "wait for work" on the ThreadsafeVirtualDom does indeed work, because this method only accesses `Send` types.
|
|
||||||
/// Otherwise, the VirtualDom must be unlocked on the current thread to modify any data.
|
|
||||||
///
|
|
||||||
/// Dioxus does have the concept of local tasks and non-local tasks.
|
|
||||||
///
|
|
||||||
/// For the ThreadsafeVirtualDom, non-Send tasks are not ran - and will error out during a Debug build if one is submitted.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// When Tasks are submitted to a thread-local executor,
|
|
||||||
///
|
|
||||||
pub struct ThreadsafeVirtualDom {
|
|
||||||
inner: Arc<Mutex<VirtualDom>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThreadsafeVirtualDom {
|
|
||||||
pub fn new(inner: VirtualDom) -> Self {
|
|
||||||
let inner = Arc::new(Mutex::new(inner));
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lock(&self) -> Option<VirtualDomGuard> {
|
|
||||||
let locked = self.inner.lock().unwrap();
|
|
||||||
Some(VirtualDomGuard { guard: locked })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for ThreadsafeVirtualDom {}
|
|
||||||
|
|
||||||
pub struct VirtualDomGuard<'a> {
|
|
||||||
guard: MutexGuard<'a, VirtualDom>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> std::ops::Deref for VirtualDomGuard<'a> {
|
|
||||||
type Target = MutexGuard<'a, VirtualDom>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.guard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> std::ops::DerefMut for VirtualDomGuard<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.guard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,6 +94,14 @@ pub struct SelfReferentialItems<'a> {
|
|||||||
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
|
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A component's unique identifier.
|
||||||
|
///
|
||||||
|
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
||||||
|
/// unmounted, then the `ScopeId` will be reused for a new component.
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct ScopeId(pub usize);
|
||||||
|
|
||||||
// Public methods exposed to libraries and components
|
// Public methods exposed to libraries and components
|
||||||
impl Scope {
|
impl Scope {
|
||||||
/// Get the subtree ID that this scope belongs to.
|
/// Get the subtree ID that this scope belongs to.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
// create a cell with a "none" value
|
|
||||||
#[inline]
|
|
||||||
pub fn empty_cell() -> Cell<Option<ElementId>> {
|
|
||||||
Cell::new(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A component's unique identifier.
|
|
||||||
///
|
|
||||||
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
|
||||||
/// unmounted, then the `ScopeId` will be reused for a new component.
|
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub struct ScopeId(pub usize);
|
|
||||||
|
|
||||||
/// An Element's unique identifier.
|
|
||||||
///
|
|
||||||
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
|
||||||
/// unmounted, then the `ElementId` will be reused for a new component.
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub struct ElementId(pub usize);
|
|
||||||
impl Display for ElementId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ElementId {
|
|
||||||
pub fn as_u64(self) -> u64 {
|
|
||||||
self.0 as u64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -447,18 +447,13 @@ impl VirtualDom {
|
|||||||
|
|
||||||
let scope_id = self.base_scope;
|
let scope_id = self.base_scope;
|
||||||
if self.scopes.run_scope(&scope_id) {
|
if self.scopes.run_scope(&scope_id) {
|
||||||
todo!();
|
diff_state
|
||||||
// diff_state
|
.stack
|
||||||
// .stack
|
.create_node(self.scopes.fin_head(&scope_id), MountType::Append);
|
||||||
// .create_node(self.scopes.fin_head(&scope_id), MountType::Append);
|
|
||||||
|
|
||||||
// diff_state.stack.scope_stack.push(scope_id);
|
diff_state.stack.scope_stack.push(scope_id);
|
||||||
|
|
||||||
let work_completed = diff_state.work(|| false);
|
let work_completed = diff_state.work(|| false);
|
||||||
// let scopes = &mut self.scopes;
|
|
||||||
//
|
|
||||||
// let work_completed = crate::diff::work(&mut diff_state, || false);
|
|
||||||
// self.scopes.work(&mut diff_machine, || false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_state.mutations
|
diff_state.mutations
|
||||||
|
|||||||
Reference in new issue
Block a user