wip: cleanup public apis

This commit is contained in:
Jonathan Kelley 2021-08-27 09:40:04 -04:00
parent 8b0eb87c72
commit 927b05f358
13 changed files with 178 additions and 371 deletions

View file

@ -16,9 +16,6 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.1" }
# Bumpalo is used as a micro heap backing each component
bumpalo = { version = "3.6.0", features = ["collections", "boxed"] }
# custom error type
thiserror = "1"
# faster hashmaps
fxhash = "0.2.1"

View file

@ -40,42 +40,3 @@ We have big goals for Dioxus. The final implementation must:
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
## Optimizations
- Support a pluggable allocation strategy that makes VNode creation **very** fast
- Support lazy VNodes (ie VNodes that are not actually created when the html! macro is used)
- Support advanced diffing strategies (patience, Meyers, etc)
```rust
rsx!{ "this is a text node" }
rsx!{
div {}
"asd"
div {}
div {}
}
rsx!{
div {
a {}
b {}
c {}
Container {
Container {
Container {
Container {
Container {
div {}
}
}
}
}
}
}
}
```

View file

@ -1,78 +0,0 @@
//! Debug virtual doms!
//! This renderer comes built in with dioxus core and shows how to implement a basic renderer.
//!
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
use crate::innerlude::RealDom;
use crate::{events::EventTrigger, virtual_dom::VirtualDom};
use crate::{innerlude::Result, prelude::*};
pub struct DebugRenderer {
internal_dom: VirtualDom,
}
impl DebugRenderer {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new_with_props<T: Properties + 'static>(root: FC<T>, root_props: T) -> Self {
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
}
/// Create a new text renderer from an existing Virtual DOM.
pub fn from_vdom(dom: VirtualDom) -> Self {
// todo: initialize the event registry properly
Self { internal_dom: dom }
}
pub fn handle_event(&mut self, trigger: EventTrigger) -> Result<()> {
Ok(())
}
// pub fn step<Dom: RealDom>(&mut self, machine: &mut DiffMachine<Dom>) -> Result<()> {
// Ok(())
// }
// this does a "holy" compare - if something is missing in the rhs, it doesn't complain.
// it only complains if something shows up that's not in the lhs, *or* if a value is different.
// This lets you exclude various fields if you just want to drill in to a specific prop
// It leverages the internal diffing mechanism.
// If you have a list or "nth" child, you do need to list those children, but you don't need to
// fill in their children/attrs/etc
// Does not handle children or lifecycles and will always fail the test if they show up in the rhs
pub fn compare<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
// Do a full compare - everything must match
// Ignores listeners and children components
pub fn compare_full<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
pub fn trigger_listener(&mut self, id: usize) -> Result<()> {
Ok(())
}
pub fn render_nodes<F>(&self, other: LazyNodes<F>) -> Result<()>
where
F: for<'b, 'c> FnOnce(&'b NodeFactory<'c>) -> VNode<'c>,
{
Ok(())
}
}

View file

@ -105,7 +105,7 @@ use DomEdit::*;
///
/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's
/// stack machines all the way down!
pub struct DiffMachine<'bump> {
pub(crate) struct DiffMachine<'bump> {
pub vdom: &'bump ResourcePool,
pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>,
@ -115,7 +115,7 @@ pub struct DiffMachine<'bump> {
/// a "saved" form of a diff machine
/// in regular diff machine, the &'bump reference is a stack borrow, but the
/// bump lifetimes are heap borrows.
pub struct SavedDiffWork<'bump> {
pub(crate) struct SavedDiffWork<'bump> {
pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>,
pub seen_scopes: FxHashSet<ScopeId>,
@ -174,36 +174,23 @@ impl<'bump> DiffMachine<'bump> {
///
/// We do depth-first to maintain high cache locality (nodes were originally generated recursively).
pub async fn work(&mut self) {
// defer to individual functions so the compiler produces better code
// large functions tend to be difficult for the compiler to work with
while let Some(instruction) = self.stack.pop() {
// defer to individual functions so the compiler produces better code
// large functions tend to be difficult for the compiler to work with
match instruction {
DiffInstruction::PopScope => {
self.stack.pop_scope();
}
DiffInstruction::DiffNode { old, new, .. } => {
self.diff_node(old, new);
}
DiffInstruction::DiffNode { old, new, .. } => self.diff_node(old, new),
DiffInstruction::DiffChildren { old, new } => {
self.diff_children(old, new);
}
DiffInstruction::DiffChildren { old, new } => self.diff_children(old, new),
DiffInstruction::Create { node } => {
self.create_node(node);
}
DiffInstruction::Create { node } => self.create_node(node),
DiffInstruction::Mount { and } => {
self.mount(and);
}
DiffInstruction::Mount { and } => self.mount(and),
DiffInstruction::PrepareMoveNode { node } => {
for el in RealChildIterator::new(node, self.vdom) {
self.mutations.push_root(el.direct_id());
self.stack.add_child_count(1);
}
}
DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node),
};
// todo: call this less frequently, there is a bit of overhead involved
@ -211,6 +198,13 @@ impl<'bump> DiffMachine<'bump> {
}
}
fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) {
for el in RealChildIterator::new(node, self.vdom) {
self.mutations.push_root(el.direct_id());
self.stack.add_child_count(1);
}
}
fn mount(&mut self, and: MountType<'bump>) {
let nodes_created = self.stack.pop_nodes_created();
match and {
@ -363,22 +357,12 @@ impl<'bump> DiffMachine<'bump> {
let new_component = self.vdom.get_scope_mut(new_idx).unwrap();
// Run the scope for one iteration to initialize it
match new_component.run_scope() {
Ok(_g) => {
// all good, new nodes exist
}
Err(err) => {
// failed to run. this is the first time the component ran, and it failed
// we manually set its head node to an empty fragment
panic!("failing components not yet implemented");
}
if new_component.run_scope() {
// Take the node that was just generated from running the component
let nextnode = new_component.frames.fin_head();
self.stack.create_component(new_idx, nextnode);
}
// Take the node that was just generated from running the component
let nextnode = new_component.frames.fin_head();
self.stack.create_component(new_idx, nextnode);
// Finally, insert this scope as a seen node.
self.seen_scopes.insert(new_idx);
}
@ -545,8 +529,9 @@ impl<'bump> DiffMachine<'bump> {
}
false => {
// the props are different...
scope.run_scope().unwrap();
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
if scope.run_scope() {
self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
}
}
}

View file

@ -40,7 +40,7 @@ pub enum MountType<'a> {
InsertBefore { other_node: &'a VNode<'a> },
}
pub struct DiffStack<'bump> {
pub(crate) struct DiffStack<'bump> {
instructions: Vec<DiffInstruction<'bump>>,
nodes_created_stack: SmallVec<[usize; 10]>,
pub scope_stack: SmallVec<[ScopeId; 5]>,

View file

@ -1,105 +0,0 @@
//!
//!
//!
//!
//!
//!
use crate::innerlude::ScopeId;
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
PushRoot {
id: u64,
},
PopRoot,
AppendChildren {
many: u32,
},
// "Root" refers to the item direclty
// it's a waste of an instruction to push the root directly
ReplaceWith {
root: u64,
m: u32,
},
InsertAfter {
root: u64,
n: u32,
},
InsertBefore {
root: u64,
n: u32,
},
Remove {
root: u64,
},
RemoveAllChildren,
CreateTextNode {
text: &'bump str,
id: u64,
},
CreateElement {
tag: &'bump str,
id: u64,
},
CreateElementNs {
tag: &'bump str,
id: u64,
ns: &'static str,
},
CreatePlaceholder {
id: u64,
},
NewEventListener {
event_name: &'static str,
scope: ScopeId,
mounted_node_id: u64,
},
RemoveEventListener {
event: &'static str,
},
SetText {
text: &'bump str,
},
SetAttribute {
field: &'static str,
value: &'bump str,
ns: Option<&'bump str>,
},
RemoveAttribute {
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::InsertAfter { .. } => id == "InsertAfter",
DomEdit::InsertBefore { .. } => id == "InsertBefore",
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove { .. } => id == "Remove",
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}

View file

@ -1,30 +0,0 @@
//! Internal error handling for Dioxus
//!
//!
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
#[error("Fatal Internal Error: {0}")]
FatalInternal(&'static str),
#[error("Context is missing")]
MissingSharedContext,
#[error("No event to progress")]
NoEvent,
#[error("Wrong Properties Type")]
WrongProps,
#[error("The component failed to return VNodes")]
ComponentFailed,
#[error("Base scope has not been mounted yet")]
NotMounted,
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
}

View file

@ -1,14 +1,5 @@
#![allow(non_snake_case)]
#![doc = include_str!("../README.md")]
//! Dioxus Core
//! ----------
//!
//!
//!
//!
//!
//!
//!
pub use crate::innerlude::{
format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId,
@ -26,7 +17,6 @@ pub mod prelude {
pub use dioxus_core_macro::{format_args_f, html, rsx, Props};
}
// types used internally that are important
pub(crate) mod innerlude {
pub use crate::bumpframe::*;
pub use crate::childiter::*;
@ -34,8 +24,6 @@ pub(crate) mod innerlude {
pub use crate::context::*;
pub use crate::diff::*;
pub use crate::diff_stack::*;
pub use crate::dom_edits::*;
pub use crate::error::*;
pub use crate::events::*;
pub use crate::heuristics::*;
pub use crate::hooklist::*;
@ -43,7 +31,6 @@ pub(crate) mod innerlude {
pub use crate::mutations::*;
pub use crate::nodes::*;
pub use crate::scheduler::*;
// pub use crate::scheduler::*;
pub use crate::scope::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
@ -57,19 +44,29 @@ pub(crate) mod innerlude {
pub mod exports {
//! Important dependencies that are used by the rest of the library
// the foundation of this library
pub use bumpalo;
}
/*
Navigating this crate:
- virtual_dom: the primary entrypoint for the crate
- scheduler: the core interior logic called by virtual_dom
- nodes: the definition of VNodes, listeners, etc.
-
Some utilities
*/
pub mod bumpframe;
pub mod childiter;
pub mod component;
pub mod context;
pub mod diff;
pub mod diff_stack;
pub mod dom_edits;
pub mod error;
pub mod events;
pub mod heuristics;
pub mod hooklist;

View file

@ -163,3 +163,100 @@ impl<'a> NodeRefMutation<'a> {
.and_then(|f| f.downcast_mut::<T>())
}
}
/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
PushRoot {
id: u64,
},
PopRoot,
AppendChildren {
many: u32,
},
// "Root" refers to the item direclty
// it's a waste of an instruction to push the root directly
ReplaceWith {
root: u64,
m: u32,
},
InsertAfter {
root: u64,
n: u32,
},
InsertBefore {
root: u64,
n: u32,
},
Remove {
root: u64,
},
RemoveAllChildren,
CreateTextNode {
text: &'bump str,
id: u64,
},
CreateElement {
tag: &'bump str,
id: u64,
},
CreateElementNs {
tag: &'bump str,
id: u64,
ns: &'static str,
},
CreatePlaceholder {
id: u64,
},
NewEventListener {
event_name: &'static str,
scope: ScopeId,
mounted_node_id: u64,
},
RemoveEventListener {
event: &'static str,
},
SetText {
text: &'bump str,
},
SetAttribute {
field: &'static str,
value: &'bump str,
ns: Option<&'bump str>,
},
RemoveAttribute {
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::InsertAfter { .. } => id == "InsertAfter",
DomEdit::InsertBefore { .. } => id == "InsertBefore",
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove { .. } => id == "Remove",
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}

View file

@ -128,7 +128,12 @@ pub enum SchedulerMsg {
/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task.
///
///
pub struct Scheduler {
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 guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub pool: ResourcePool,
pub heuristics: HeuristicsEngine,
@ -523,7 +528,7 @@ impl Scheduler {
}
}
pub struct PriorityLane {
pub(crate) struct PriorityLane {
pub dirty_scopes: IndexSet<ScopeId>,
pub saved_state: Option<SavedDiffWork<'static>>,
pub in_progress: bool,
@ -682,7 +687,7 @@ impl ResourcePool {
&'b self,
_id: ScopeId,
_f: impl FnOnce(&'b mut Scope) -> O,
) -> Result<O> {
) -> Option<O> {
todo!()
}
@ -692,13 +697,13 @@ impl ResourcePool {
&self,
_id: ScopeId,
_f: impl FnOnce(&mut Scope) -> &VNode<'b>,
) -> Result<&VNode<'b>> {
) -> Option<&VNode<'b>> {
todo!()
}
pub fn try_remove(&self, id: ScopeId) -> Result<Scope> {
pub fn try_remove(&self, id: ScopeId) -> Option<Scope> {
let inner = unsafe { &mut *self.components.get() };
Ok(inner.remove(id.0))
Some(inner.remove(id.0))
// .try_remove(id.0)
// .ok_or_else(|| Error::FatalInternal("Scope not found"))
}

View file

@ -113,7 +113,9 @@ impl Scope {
self.child_nodes = child_nodes;
}
pub(crate) fn run_scope<'sel>(&'sel mut self) -> Result<()> {
/// Returns true if the scope completed successfully
///
pub(crate) fn run_scope<'sel>(&'sel mut self) -> bool {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
@ -133,17 +135,12 @@ impl Scope {
let render: &WrappedCaller = self.caller.as_ref();
match render(self) {
None => {
// the user's component failed. We avoid cycling to the next frame
log::error!("Running your component failed! It will no longer receive events.");
Err(Error::ComponentFailed)
}
None => false,
Some(new_head) => {
// the user's component succeeded. We can safely cycle to the next frame
self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
self.frames.cycle_frame();
log::debug!("Successfully rendered component");
Ok(())
true
}
}
}
@ -202,7 +199,7 @@ impl Scope {
&mut self,
event: SyntheticEvent,
element: ElementId,
) -> Result<()> {
) -> Option<()> {
let listners = self.listeners.borrow_mut();
let raw_listener = listners.iter().find(|lis| {
@ -226,7 +223,7 @@ impl Scope {
log::warn!("An event was triggered but there was no listener to handle it");
}
Ok(())
Some(())
}
pub fn root(&self) -> &VNode {

View file

@ -11,22 +11,3 @@ pub fn empty_cell() -> Cell<Option<ElementId>> {
pub fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
// /// A helper type that lets scopes be ordered by their height
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
// pub struct HeightMarker {
// pub idx: ScopeId,
// pub height: u32,
// }
// impl Ord for HeightMarker {
// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// self.height.cmp(&other.height)
// }
// }
// impl PartialOrd for HeightMarker {
// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// Some(self.cmp(other))
// }
// }

View file

@ -18,6 +18,7 @@
//!
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
use crate::innerlude::*;
use futures_util::{pin_mut, Future, FutureExt};
use std::{
@ -35,23 +36,13 @@ use std::{
///
///
pub struct VirtualDom {
/// 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 guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually.
pub scheduler: Scheduler,
scheduler: Scheduler,
/// The index of the root component
/// Should always be the first (gen=0, id=0)
base_scope: ScopeId,
// for managing the props that were used to create the dom
#[doc(hidden)]
_root_prop_type: std::any::TypeId,
root_prop_type: std::any::TypeId,
#[doc(hidden)]
_root_props: std::pin::Pin<Box<dyn std::any::Any>>,
root_props: Pin<Box<dyn std::any::Any>>,
}
impl VirtualDom {
@ -67,8 +58,8 @@ impl VirtualDom {
///
/// # Example
/// ```
/// fn Example(cx: Context<SomeProps>) -> VNode {
/// cx.render(rsx!{ div{"hello world"} })
/// fn Example(cx: Context<()>) -> DomTree {
/// cx.render(rsx!( div { "hello world" } ))
/// }
///
/// let dom = VirtualDom::new(Example);
@ -91,25 +82,34 @@ impl VirtualDom {
///
/// # Example
/// ```
/// fn Example(cx: Context<SomeProps>) -> VNode {
/// cx.render(rsx!{ div{"hello world"} })
/// #[derive(PartialEq, Props)]
/// struct SomeProps {
/// name: &'static str
/// }
///
/// fn Example(cx: Context<SomeProps>) -> DomTree {
/// cx.render(rsx!{ div{ "hello {cx.name}" } })
/// }
///
/// let dom = VirtualDom::new(Example);
/// ```
///
/// Note: the VirtualDOM is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
/// Note: the VirtualDOM is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it.
///
/// ```rust
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
let scheduler = Scheduler::new();
let _root_props: Pin<Box<dyn Any>> = Box::pin(root_props);
let _root_prop_type = TypeId::of::<P>();
let props_ptr = _root_props.as_ref().downcast_ref::<P>().unwrap() as *const P;
let props_ptr = _root_props.downcast_ref::<P>().unwrap() as *const P;
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
let name = type_name_of(root);
Scope::new(
caller,
myidx,
@ -122,9 +122,9 @@ impl VirtualDom {
Self {
base_scope,
_root_props,
scheduler,
_root_prop_type,
root_props: _root_props,
root_prop_type: _root_prop_type,
}
}
@ -168,7 +168,7 @@ impl VirtualDom {
.expect("The base scope should never be moved");
// // We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope().is_ok() {
if cur_component.run_scope() {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
@ -204,9 +204,9 @@ impl VirtualDom {
.get_scope_mut(self.base_scope)
.expect("The base scope should never be moved");
cur_component.run_scope().unwrap();
diff_machine.diff_scope(self.base_scope).await;
if cur_component.run_scope() {
diff_machine.diff_scope(self.base_scope).await;
}
diff_machine.mutations
}