polish: tests and documentation

This commit is contained in:
Jonathan Kelley committed 2021-09-09 20:58:48 -04:00
1 parent 0896f34493
commit da8159190b
27 files changed
+455 -557

No files matched your search

+1
View File
@@ -66,3 +66,4 @@ constified
SegVec
contentful
Jank
noderef
+2
View File
@@ -109,6 +109,8 @@ If you know React, then you already know Dioxus.
- Sierpinski's triangle (web SPA)
- Doxie Documentation Library (Web SPA with Hydration)
See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem.
<!--
currently commented out until we have more content on the website
## Explore
+5
View File
@@ -14,6 +14,7 @@ pub(crate) struct BumpFrame {
pub bump: Bump,
pub(crate) head_node: VNode<'static>,
#[cfg(test)]
// used internally for debugging
_name: &'static str,
}
@@ -23,11 +24,15 @@ impl ActiveFrame {
let frame_a = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
#[cfg(test)]
_name: "wip",
};
let frame_b = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
#[cfg(test)]
_name: "fin",
};
Self {
-1
View File
@@ -64,7 +64,6 @@ impl<'a> Iterator for RealChildIterator<'a> {
.scopes
.get_scope(sc.associated_scope.get().unwrap())
.unwrap();
// let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.frames.fin_head();
+27 -22
View File
@@ -7,6 +7,32 @@
use crate::innerlude::{Context, DomTree, LazyNodes, FC};
/// Create inline fragments using Component syntax.
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// # Example
///
/// ```rust
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
///
/// # Details
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid nesting fragments if you can. There is no protection against infinitely nested fragments.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
///
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> {
cx.render(LazyNodes::new(|f| f.fragment_from_iter(cx.children())))
}
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
@@ -44,7 +70,7 @@ pub trait Properties: Sized {
const IS_STATIC: bool;
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are 'static
/// Memoization can only happen if the props are valid for the 'static lifetime
/// The user must know if their props are static, but if they make a mistake, UB happens
/// Therefore it's unsafe to memeoize.
unsafe fn memoize(&self, other: &Self) -> bool;
@@ -75,24 +101,3 @@ impl EmptyBuilder {
pub fn fc_to_builder<T: Properties>(_: FC<T>) -> T::Builder {
T::builder()
}
/// Create inline fragments
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
///
/// ```rust
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> {
cx.render(LazyNodes::new(move |f| f.fragment_from_iter(cx.children())))
}
+47 -105
View File
@@ -124,6 +124,7 @@ impl<'a> SavedDiffWork<'a> {
pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> {
std::mem::transmute(self)
}
pub unsafe fn promote<'b>(self, vdom: &'b mut ResourcePool) -> DiffMachine<'b> {
let extended: SavedDiffWork<'b> = std::mem::transmute(self);
DiffMachine {
@@ -136,10 +137,10 @@ impl<'a> SavedDiffWork<'a> {
}
impl<'bump> DiffMachine<'bump> {
pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump ResourcePool) -> Self {
pub(crate) fn new(mutations: Mutations<'bump>, shared: &'bump ResourcePool) -> Self {
Self {
mutations,
stack: DiffStack::new(),
mutations: edits,
vdom: shared,
seen_scopes: FxHashSet::default(),
}
@@ -156,7 +157,7 @@ impl<'bump> DiffMachine<'bump> {
pub fn diff_scope(&mut self, id: ScopeId) {
if let Some(component) = self.vdom.get_scope_mut(id) {
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
self.stack.push(DiffInstruction::DiffNode { new, old });
self.stack.push(DiffInstruction::Diff { new, old });
}
}
@@ -167,24 +168,14 @@ impl<'bump> DiffMachine<'bump> {
/// We do depth-first to maintain high cache locality (nodes were originally generated recursively).
///
/// Returns a `bool` indicating that the work completed properly.
pub fn work(&mut self, deadline_expired: &mut impl FnMut() -> bool) -> bool {
pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool {
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::DiffChildren { old, new } => self.diff_children(old, new),
DiffInstruction::Diff { old, new } => self.diff_node(old, new),
DiffInstruction::Create { node } => self.create_node(node),
DiffInstruction::Mount { and } => self.mount(and),
DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node),
DiffInstruction::PrepareMove { node } => self.prepare_move_node(node),
DiffInstruction::PopScope => self.stack.pop_off_scope(),
};
if deadline_expired() {
@@ -217,16 +208,22 @@ impl<'bump> DiffMachine<'bump> {
}
MountType::Replace { old } => {
if let Some(old_id) = old.try_mounted_id() {
self.mutations.replace_with(old_id, nodes_created as u32);
} else {
let mut iter = RealChildIterator::new(old, self.vdom);
let first = iter.next().unwrap();
self.mutations
.replace_with(first.mounted_id(), nodes_created as u32);
self.remove_nodes(iter);
}
}
MountType::ReplaceByElementId { el: old } => {
MountType::ReplaceByElementId { el } => {
if let Some(old) = el {
self.mutations.replace_with(old, nodes_created as u32);
}
}
MountType::InsertAfter { other_node } => {
let root = self.find_last_element(other_node).unwrap();
@@ -326,6 +323,7 @@ impl<'bump> DiffMachine<'bump> {
let parent_idx = self.stack.current_scope().unwrap();
let shared = self.vdom.channel.clone();
// Insert a new scope into our component list
let parent_scope = self.vdom.get_scope(parent_idx).unwrap();
let new_idx = self.vdom.insert_scope_with_key(|new_idx| {
@@ -376,7 +374,9 @@ impl<'bump> DiffMachine<'bump> {
match (old_node, new_node) {
// Check the most common cases first
(Text(old), Text(new)) => self.diff_text_nodes(old, new),
(Component(old), Component(new)) => self.diff_component_nodes(old, new),
(Component(old), Component(new)) => {
self.diff_component_nodes(old_node, new_node, old, new)
}
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
(Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()),
(Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()),
@@ -393,8 +393,7 @@ impl<'bump> DiffMachine<'bump> {
}
fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) {
let root = old.dom_id.get().unwrap();
if let Some(root) = old.dom_id.get() {
if old.text != new.text {
self.mutations.push_root(root);
self.mutations.set_text(new.text);
@@ -403,9 +402,10 @@ impl<'bump> DiffMachine<'bump> {
new.dom_id.set(Some(root));
}
}
fn diff_element_nodes(&mut self, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>) {
let root = old.dom_id.get().unwrap();
let root = old.dom_id.get();
// If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code
@@ -417,22 +417,24 @@ impl<'bump> DiffMachine<'bump> {
self.stack.push_nodes_created(0);
self.stack.push(DiffInstruction::Mount {
and: MountType::ReplaceByElementId {
el: old.dom_id.get().unwrap(),
el: old.dom_id.get(),
},
});
self.create_element_node(new);
return;
}
new.dom_id.set(Some(root));
new.dom_id.set(root);
// Don't push the root if we don't have to
let mut has_comitted = false;
let mut please_commit = |edits: &mut Vec<DomEdit>| {
if !has_comitted {
has_comitted = true;
if let Some(root) = root {
edits.push(PushRoot { id: root.as_u64() });
}
}
};
// Diff Attributes
@@ -449,7 +451,6 @@ impl<'bump> DiffMachine<'bump> {
}
}
} else {
// TODO: provide some sort of report on how "good" the diffing was
please_commit(&mut self.mutations.edits);
for attribute in old.attributes {
self.mutations.remove_attribute(attribute);
@@ -467,7 +468,6 @@ impl<'bump> DiffMachine<'bump> {
// We also need to make sure that all listeners are properly attached to the parent scope (fix_listener)
//
// TODO: take a more efficient path than this
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
@@ -487,22 +487,21 @@ impl<'bump> DiffMachine<'bump> {
self.mutations.remove_event_listener(listener.event);
}
for listener in new.listeners {
listener.mounted_node.set(Some(root));
listener.mounted_node.set(root);
self.mutations.new_event_listener(listener, cur_scope_id);
self.attach_listener_to_scope(listener, scope);
}
}
}
if has_comitted {
self.mutations.pop();
}
self.diff_children(old.children, new.children);
}
fn diff_component_nodes(
&mut self,
old_node: &'bump VNode<'bump>,
new_node: &'bump VNode<'bump>,
old: &'bump VComponent<'bump>,
new: &'bump VComponent<'bump>,
) {
@@ -526,7 +525,7 @@ impl<'bump> DiffMachine<'bump> {
match compare(new) {
true => {
// the props are the same...
// the props are the same... do nothing
}
false => {
// the props are different...
@@ -540,25 +539,8 @@ impl<'bump> DiffMachine<'bump> {
self.seen_scopes.insert(scope_addr);
} else {
todo!();
// let mut old_iter = RealChildIterator::new(old_node, &self.vdom);
// let first = old_iter
// .next()
// .expect("Components should generate a placeholder root");
// // remove any leftovers
// for to_remove in old_iter {
// self.mutations.push_root(to_remove.direct_id());
// self.mutations.remove();
// }
// // seems like we could combine this into a single instruction....
// self.replace_node_with_node(first.direct_id(), old_node, new_node);
// // Wipe the old one and plant the new one
// let old_scope = old.ass_scope.get().unwrap();
// self.destroy_scopes(old_scope);
self.stack
.create_node(new_node, MountType::Replace { old: old_node });
}
}
@@ -608,7 +590,7 @@ impl<'bump> DiffMachine<'bump> {
old_anchor.dom_id.set(new_anchor.dom_id.get());
}
([VNode::Anchor(anchor)], _) => {
let el = anchor.dom_id.get().unwrap();
let el = anchor.dom_id.get();
self.stack
.create_children(new, MountType::ReplaceByElementId { el });
}
@@ -652,7 +634,7 @@ impl<'bump> DiffMachine<'bump> {
debug_assert!(!old.is_empty());
for (new, old) in new.iter().zip(old.iter()).rev() {
self.stack.push(DiffInstruction::DiffNode { new, old });
self.stack.push(DiffInstruction::Diff { new, old });
}
if old.len() > new.len() {
@@ -748,7 +730,7 @@ impl<'bump> DiffMachine<'bump> {
if old.key() != new.key() {
break;
}
self.stack.push(DiffInstruction::DiffNode { old, new });
self.stack.push(DiffInstruction::Diff { old, new });
left_offset += 1;
}
@@ -887,8 +869,8 @@ impl<'bump> DiffMachine<'bump> {
stack.create_node(new_node, MountType::Absorb);
} else {
// this funciton should never take LIS indicies
stack.push(DiffInstruction::PrepareMoveNode { node: new_node });
stack.push(DiffInstruction::DiffNode {
stack.push(DiffInstruction::PrepareMove { node: new_node });
stack.push(DiffInstruction::Diff {
new: new_node,
old: &old[old_index],
});
@@ -943,7 +925,7 @@ impl<'bump> DiffMachine<'bump> {
}
for idx in lis_sequence.iter().rev() {
self.stack.push(DiffInstruction::DiffNode {
self.stack.push(DiffInstruction::Diff {
new: &new[*idx],
old: &old[new_index_to_old_index[*idx]],
});
@@ -998,6 +980,14 @@ impl<'bump> DiffMachine<'bump> {
}
}
fn replace_and_create_one_with_many(
&mut self,
old: &'bump VNode<'bump>,
new: &'bump [VNode<'bump>],
) {
//
}
fn replace_and_create_many_with_one(
&mut self,
old: &'bump [VNode<'bump>],
@@ -1070,60 +1060,12 @@ impl<'bump> DiffMachine<'bump> {
}
}
fn create_garbage(&mut self, node: &'bump VNode<'bump>) {
match self
.stack
.current_scope()
.and_then(|id| self.vdom.get_scope(id))
{
Some(scope) => {
let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) };
scope.pending_garbage.borrow_mut().push(garbage);
}
None => {
log::info!("No scope to collect garbage into")
}
}
}
/// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &Scope) {
let mut queue = scope.listeners.borrow_mut();
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) };
queue.push(long_listener as *const _)
}
/// Destroy a scope and all of its descendents.
///
/// Calling this will run the destuctors on all hooks in the tree.
/// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered.
fn destroy_scopes(&mut self, old_scope: ScopeId) {
let mut nodes_to_delete = vec![old_scope];
let mut scopes_to_explore = vec![old_scope];
// explore the scope tree breadth first
while let Some(scope_id) = scopes_to_explore.pop() {
// If we're planning on deleting this node, then we don't need to both rendering it
self.seen_scopes.insert(scope_id);
let scope = self.vdom.get_scope(scope_id).unwrap();
for child in scope.descendents.borrow().iter() {
// Add this node to be explored
scopes_to_explore.push(child.clone());
// Also add it for deletion
nodes_to_delete.push(child.clone());
}
}
// Delete all scopes that we found as part of this subtree
for node in nodes_to_delete {
log::debug!("Removing scope {:#?}", node);
let _scope = self.vdom.try_remove(node).unwrap();
// do anything we need to do to delete the scope
// I think we need to run the destructors on the hooks
// TODO
}
}
}
fn compare_strs(a: &str, b: &str) -> bool {
+8 -9
View File
@@ -3,23 +3,18 @@ use smallvec::{smallvec, SmallVec};
/// The stack instructions we use to diff and create new nodes.
#[derive(Debug)]
pub enum DiffInstruction<'a> {
DiffNode {
pub(crate) enum DiffInstruction<'a> {
Diff {
old: &'a VNode<'a>,
new: &'a VNode<'a>,
},
DiffChildren {
old: &'a [VNode<'a>],
new: &'a [VNode<'a>],
},
Create {
node: &'a VNode<'a>,
},
/// pushes the node elements onto the stack for use in mount
PrepareMoveNode {
PrepareMove {
node: &'a VNode<'a>,
},
@@ -35,7 +30,7 @@ pub enum MountType<'a> {
Absorb,
Append,
Replace { old: &'a VNode<'a> },
ReplaceByElementId { el: ElementId },
ReplaceByElementId { el: Option<ElementId> },
InsertAfter { other_node: &'a VNode<'a> },
InsertBefore { other_node: &'a VNode<'a> },
}
@@ -63,6 +58,10 @@ impl<'bump> DiffStack<'bump> {
self.instructions.pop()
}
pub fn pop_off_scope(&mut self) {
self.scope_stack.pop();
}
pub fn pop_scope(&mut self) -> Option<ScopeId> {
self.scope_stack.pop()
}
+5
View File
@@ -125,8 +125,13 @@ pub mod on {
let callback: BumpBox<dyn FnMut(SyntheticEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
// ie oncopy
let event_name = stringify!($name);
// ie copy
let shortname: &'static str = &event_name[2..];
Listener {
event: shortname,
mounted_node: Cell::new(None),
+14 -30
View File
@@ -3,45 +3,28 @@ use std::{
cell::{Cell, RefCell, UnsafeCell},
};
/// An abstraction over internally stored data using a hook-based memory layout.
///
/// Hooks are allocated using Boxes and then our stored references are given out.
///
/// It's unsafe to "reset" the hooklist, but it is safe to add hooks into it.
///
/// Todo: this could use its very own bump arena, but that might be a tad overkill
#[derive(Default)]
pub(crate) struct HookList {
vals: RefCell<Vec<InnerHook<Box<dyn Any>>>>,
vals: RefCell<Vec<UnsafeCell<Box<dyn Any>>>>,
idx: Cell<usize>,
}
impl Default for HookList {
fn default() -> Self {
Self {
vals: Default::default(),
idx: Cell::new(0),
}
}
}
struct InnerHook<T> {
cell: UnsafeCell<T>,
}
impl<T> InnerHook<T> {
fn new(new: T) -> Self {
Self {
cell: UnsafeCell::new(new),
}
}
}
impl HookList {
pub(crate) fn next<T: 'static>(&self) -> Option<&mut T> {
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
self.idx.set(self.idx.get() + 1);
let raw_box = unsafe { &mut *inn.cell.get() };
let raw_box = unsafe { &mut *inn.get() };
raw_box.downcast_mut::<T>()
})
}
pub(crate) fn push<T: 'static>(&self, new: T) {
self.vals.borrow_mut().push(InnerHook::new(Box::new(new)))
}
/// This resets the internal iterator count
/// It's okay that we've given out each hook, but now we have the opportunity to give it out again
/// Therefore, resetting is cosudered unsafe
@@ -52,17 +35,18 @@ impl HookList {
self.idx.set(0);
}
#[inline]
pub(crate) fn push<T: 'static>(&self, new: T) {
self.vals.borrow_mut().push(UnsafeCell::new(Box::new(new)))
}
pub(crate) fn len(&self) -> usize {
self.vals.borrow().len()
}
#[inline]
pub(crate) fn cur_idx(&self) -> usize {
self.idx.get()
}
#[inline]
pub(crate) fn at_end(&self) -> bool {
self.cur_idx() >= self.len()
}
+3 -3
View File
@@ -56,9 +56,9 @@ pub(crate) mod innerlude {
}
pub use crate::innerlude::{
format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId,
EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId,
SuspendedContext, SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC,
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext,
SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC,
};
pub mod prelude {
+3 -3
View File
@@ -26,6 +26,7 @@ pub enum VNode<'src> {
///
/// # Example
///
/// ```
/// let node = cx.render(rsx!{ "hello" }).unwrap();
///
/// if let VNode::Text(vtext) = node {
@@ -248,7 +249,7 @@ pub struct Listener<'bump> {
/// The type of event to listen for.
///
/// IE "onclick" - whatever the renderer needs to attach the listener by name.
/// IE "click" - whatever the renderer needs to attach the listener by name.
pub event: &'static str,
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(SyntheticEvent) + 'bump>>>,
@@ -283,8 +284,7 @@ pub struct VComponent<'src> {
pub struct VSuspended<'a> {
pub task_id: u64,
pub dom_id: Cell<Option<ElementId>>,
pub(crate) callback:
RefCell<Option<BumpBox<'a, dyn FnMut(SuspendedContext<'a>) -> DomTree<'a>>>>,
pub callback: RefCell<Option<BumpBox<'a, dyn FnMut(SuspendedContext<'a>) -> DomTree<'a>>>>,
}
/// This struct provides an ergonomic API to quickly build VNodes.
+76 -84
View File
@@ -14,7 +14,7 @@ Some essential reading:
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 if being "fast" if you can't also be "responsive."
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).
@@ -87,7 +87,7 @@ use std::{
};
#[derive(Clone)]
pub struct EventChannel {
pub(crate) struct EventChannel {
pub task_counter: Rc<Cell<u64>>,
pub sender: UnboundedSender<SchedulerMsg>,
pub schedule_any_immediate: Rc<dyn Fn(ScopeId)>,
@@ -96,8 +96,13 @@ pub struct EventChannel {
}
pub enum SchedulerMsg {
Immediate(ScopeId),
// events from the host
UiEvent(UserEvent),
// setstate
Immediate(ScopeId),
// tasks
SubmitTask(FiberTask, u64),
ToggleTask(u64),
PauseTask(u64),
@@ -109,7 +114,7 @@ pub enum SchedulerMsg {
///
/// 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 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
/// 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
@@ -211,6 +216,7 @@ impl Scheduler {
Self {
pool,
receiver,
async_tasks: FuturesUnordered::new(),
@@ -219,7 +225,6 @@ impl Scheduler {
heuristics,
// a storage for our receiver to dump into
ui_events: VecDeque::new(),
pending_immediates: VecDeque::new(),
@@ -230,7 +235,7 @@ impl Scheduler {
current_priority: EventPriority::Low,
// a dedicated fiber for each priority
// sorted high to low by priority (0 = immediate, 3 = low)
lanes: [
PriorityLane::new(),
PriorityLane::new(),
@@ -248,25 +253,69 @@ impl Scheduler {
// Converts UI events into dirty scopes with various priorities
pub fn consume_pending_events(&mut self) {
// consume all events that are "continuous" to be batched
// if we run into a discrete event, then bail early
while let Some(trigger) = self.ui_events.pop_back() {
if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
if let Some(element) = trigger.mounted_dom_id {
let priority = match &trigger.event {
SyntheticEvent::ClipboardEvent(_) => {}
SyntheticEvent::CompositionEvent(_) => {}
SyntheticEvent::KeyboardEvent(_) => {}
SyntheticEvent::FocusEvent(_) => {}
SyntheticEvent::FormEvent(_) => {}
SyntheticEvent::SelectionEvent(_) => {}
SyntheticEvent::TouchEvent(_) => {}
SyntheticEvent::WheelEvent(_) => {}
SyntheticEvent::MediaEvent(_) => {}
SyntheticEvent::AnimationEvent(_) => {}
SyntheticEvent::TransitionEvent(_) => {}
SyntheticEvent::ToggleEvent(_) => {}
SyntheticEvent::MouseEvent(_) => {}
SyntheticEvent::PointerEvent(_) => {}
SyntheticEvent::GenericEvent(_) => {}
let priority = match trigger.name {
// clipboard
"copy" | "cut" | "paste" => EventPriority::Medium,
// Composition
"compositionend" | "compositionstart" | "compositionupdate" => {
EventPriority::Low
}
// Keyboard
"keydown" | "keypress" | "keyup" => EventPriority::Low,
// Focus
"focus" | "blur" => EventPriority::Low,
// Form
"change" | "input" | "invalid" | "reset" | "submit" => EventPriority::Low,
// Mouse
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend"
| "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart"
| "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove"
| "mouseout" | "mouseover" | "mouseup" => EventPriority::Low,
// Pointer
"pointerdown" | "pointermove" | "pointerup" | "pointercancel"
| "gotpointercapture" | "lostpointercapture" | "pointerenter"
| "pointerleave" | "pointerover" | "pointerout" => EventPriority::Low,
// Selection
"select" | "touchcancel" | "touchend" => EventPriority::Low,
// Touch
"touchmove" | "touchstart" => EventPriority::Low,
// Wheel
"scroll" | "wheel" => EventPriority::Low,
// Media
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
| "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata"
| "loadstart" | "pause" | "play" | "playing" | "progress"
| "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => EventPriority::Low,
// Animation
"animationstart" | "animationend" | "animationiteration" => {
EventPriority::Low
}
// Transition
"transitionend" => EventPriority::Low,
// Toggle
"toggle" => EventPriority::Low,
_ => EventPriority::Low,
};
scope.call_listener(trigger.event, element);
@@ -341,7 +390,6 @@ impl Scheduler {
self.manually_poll_events();
if !self.has_any_work() {
self.clean_up_garbage();
return committed_mutations;
}
@@ -349,7 +397,7 @@ impl Scheduler {
while self.has_any_work() {
self.shift_priorities();
self.work_on_current_lane(&mut || false, &mut committed_mutations);
self.work_on_current_lane(|| false, &mut committed_mutations);
}
committed_mutations
@@ -393,7 +441,6 @@ impl Scheduler {
// Wait for any new events if we have nothing to do
if !self.has_any_work() {
self.clean_up_garbage();
let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await;
if deadline_expired {
@@ -425,7 +472,7 @@ impl Scheduler {
/// Returns true if the lane is finished before the deadline could be met.
pub fn work_on_current_lane(
&mut self,
deadline_reached: &mut impl FnMut() -> bool,
deadline_reached: impl FnMut() -> bool,
mutations: &mut Vec<Mutations>,
) -> bool {
// Work through the current subtree, and commit the results when it finishes
@@ -448,7 +495,7 @@ impl Scheduler {
if let Some(scope) = self.current_lane().dirty_scopes.pop() {
let component = self.pool.get_scope(scope).unwrap();
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
machine.stack.push(DiffInstruction::DiffNode { new, old });
machine.stack.push(DiffInstruction::Diff { new, old });
}
}
@@ -537,61 +584,6 @@ impl Scheduler {
fn collect_garbage(&mut self, id: ElementId) {
//
}
pub fn clean_up_garbage(&mut self) {
let mut scopes_to_kill = Vec::new();
let mut garbage_list = Vec::new();
for scope in self.garbage_scopes.drain() {
let scope = self.pool.get_scope_mut(scope).unwrap();
for node in scope.consume_garbage() {
garbage_list.push(node);
}
while let Some(node) = garbage_list.pop() {
match &node {
VNode::Text(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Anchor(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Suspended(_) => {
self.pool.collect_garbage(node.mounted_id());
}
VNode::Element(el) => {
self.pool.collect_garbage(node.mounted_id());
for child in el.children {
garbage_list.push(child);
}
}
VNode::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNode::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.associated_scope.get().unwrap();
let scope = self.pool.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.drain(..) {
//
// kill em
}
}
}
pub(crate) struct PriorityLane {
@@ -619,8 +611,8 @@ impl PriorityLane {
}
pub struct TaskHandle {
pub sender: UnboundedSender<SchedulerMsg>,
pub our_id: u64,
pub(crate) sender: UnboundedSender<SchedulerMsg>,
pub(crate) our_id: u64,
}
impl TaskHandle {
-22
View File
@@ -31,7 +31,6 @@ pub struct Scope {
pub(crate) frames: ActiveFrame,
pub(crate) caller: Rc<WrappedCaller>,
pub(crate) child_nodes: ScopeChildren<'static>,
pub(crate) pending_garbage: RefCell<Vec<*const VNode<'static>>>,
// Listeners
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
@@ -90,7 +89,6 @@ impl Scope {
listeners: Default::default(),
borrowed_props: Default::default(),
descendents: Default::default(),
pending_garbage: Default::default(),
}
}
@@ -110,7 +108,6 @@ impl Scope {
// 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
self.ensure_drop_safety(pool);
// Safety:
@@ -146,12 +143,6 @@ impl Scope {
/// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
// make sure all garabge is collected before trying to proceed with anything else
debug_assert!(
self.pending_garbage.borrow().is_empty(),
"clean up your garabge please"
);
// todo!("arch changes");
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
@@ -235,19 +226,6 @@ impl Scope {
}
}
pub(crate) fn consume_garbage(&self) -> Vec<&VNode> {
self.pending_garbage
.borrow_mut()
.drain(..)
.map(|node| {
// safety: scopes cannot cycle without their garbage being collected. these nodes are safe
let node: &VNode<'static> = unsafe { &*node };
let node: &VNode = unsafe { std::mem::transmute(node) };
node
})
.collect::<Vec<_>>()
}
pub fn root(&self) -> &VNode {
self.frames.fin_head()
}
+20 -8
View File
@@ -20,17 +20,25 @@ impl TestDom {
NodeFactory::new(&self.bump)
}
pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
pub fn render_direct<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> &'a VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
self.bump
.alloc(lazy_nodes.into_vnode(NodeFactory::new(&self.bump)))
}
pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mutations = Mutations::new();
let mut machine = DiffMachine::new(mutations, &self.scheduler.pool);
machine.stack.push(DiffInstruction::DiffNode { new, old });
machine.stack.push(DiffInstruction::Diff { new, old });
machine.mutations
}
@@ -38,7 +46,7 @@ impl TestDom {
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let old = self.bump.alloc(self.render_direct(left));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
@@ -58,20 +66,18 @@ impl TestDom {
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let new = self.bump.alloc(self.render(right));
let (old, new) = (self.render(left), self.render(right));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.create_node(old, MountType::Append);
machine.work(&mut || false);
machine.work(|| false);
let create_edits = machine.mutations;
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.push(DiffInstruction::DiffNode { old, new });
machine.stack.push(DiffInstruction::Diff { old, new });
machine.work(&mut || false);
@@ -80,3 +86,9 @@ impl TestDom {
(create_edits, edits)
}
}
impl VirtualDom {
pub fn simulate(&mut self) {
//
}
}
+42 -29
View File
@@ -1,4 +1,5 @@
//! # VirtualDOM Implementation for Rust
//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
//!
//! In this file, multiple items are defined. This file is big, but should be documented well to
@@ -20,21 +21,38 @@
use crate::innerlude::*;
use futures_util::{Future, FutureExt};
use std::{
any::{Any, TypeId},
pin::Pin,
rc::Rc,
};
use std::{any::Any, pin::Pin};
/// An integrated virtual node system that progresses events and diffs UI trees.
///
/// Differences are converted into patches which a renderer can use to draw the UI.
///
/// If you are building an App with Dioxus, you probably won't want to reach for this directly, instead opting to defer
/// to a particular crate's wrapper over the [`VirtualDom`] API.
///
/// Example
/// ```rust
/// static App: FC<()> = |cx| {
/// cx.render(rsx!{
/// div {
/// "Hello World"
/// }
/// })
/// }
///
/// async fn main() {
/// let mut dom = VirtualDom::new(App);
/// let mut inital_edits = dom.rebuild();
/// initialize_screen(inital_edits);
///
///
///
///
/// loop {
/// let next_frame = TimeoutFuture::new(Duration::from_millis(16));
/// let edits = dom.run_with_deadline(next_frame).await;
/// apply_edits(edits);
/// render_frame();
/// }
/// }
/// ```
pub struct VirtualDom {
scheduler: Scheduler,
@@ -42,7 +60,7 @@ pub struct VirtualDom {
root_fc: Box<dyn Any>,
root_props: Pin<Box<dyn std::any::Any>>,
root_props: Pin<Box<dyn Any>>,
}
impl VirtualDom {
@@ -178,28 +196,22 @@ impl VirtualDom {
}
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch
///
/// The diff machine expects the RealDom's stack to be the root of the application
/// The diff machine expects the RealDom's stack to be the root of the application.
///
/// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed
/// through "run". We completely avoid the task scheduler infrastructure.
/// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
/// root component will be ran once and then diffed. All updates will flow out as mutations.
///
/// # Example
/// ```
/// static App: FC<()> = |cx| cx.render(rsx!{ "hello world" });
/// let mut dom = VirtualDom::new();
/// let edits = dom.rebuild();
///
/// apply_edits(edits);
/// ```
pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> {
let mut fut = self.rebuild_async().boxed_local();
loop {
if let Some(edits) = (&mut fut).now_or_never() {
break edits;
}
}
}
/// Rebuild the dom from the ground up
///
/// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing
/// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This
/// method can be useful when needing to perform some crucial periodic tasks.
pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> {
let mut shared = self.scheduler.pool.clone();
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
@@ -214,9 +226,10 @@ impl VirtualDom {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
diff_machine.stack.scope_stack.push(self.base_scope);
// let completed = diff_machine.work();
diff_machine.work(|| false);
} else {
// todo: should this be a hard error?
log::warn!(
+53
View File
@@ -0,0 +1,53 @@
# Testing of Dioxus core
NodeFactory
- [] rsx, html, NodeFactory generate the same structures
Diffing
- [x] create elements
- [x] create text
- [x] create fragments
- [x] create empty fragments (placeholders)
- [x] diff elements
- [x] diff from element/text to fragment
- [x] diff from element/text to empty fragment
- [x] diff to element with children works too
- [x] replace with works forward
- [x] replace with works backward
- [x] un-keyed diffing
- [x] keyed diffing
- [x] keyed diffing out of order
- [x] keyed diffing with prefix/suffix
- [x] suspended nodes work
Lifecycle
- [] Components mount properly
- [] Components create new child components
- [] Replaced components unmount old components and mount new
- [] Post-render effects are called
- []
Shared Context
- [] Shared context propagates downwards
- [] unwrapping shared context if it doesn't exist works too
Suspense
- [] use_suspense generates suspended nodes
Hooks
- [] Drop order is maintained
- [] Shared hook state is okay
- [] use_hook works
- [] use_ref works
- [] use_noderef works
- [] use_provide_state
- [] use_consume_state
VirtualDOM API
- [] work
- [] rebuild
- [] change props
-19
View File
@@ -1,19 +0,0 @@
use futures_channel::mpsc::unbounded;
#[async_std::test]
async fn channels() {
let (sender, mut receiver) = unbounded::<u32>();
// drop(sender);
match receiver.try_next() {
Ok(a) => {
dbg!(a);
}
Err(no) => {
dbg!(no);
}
}
sender.unbounded_send(1).unwrap();
}
@@ -1,17 +1,23 @@
//! tests to prove that the iterative implementation works
//! Prove that the dom works normally through virtualdom methods.
//! This methods all use "rebuild" which completely bypasses the scheduler.
//! Hard rebuilds don't consume any events from the event queue.
use anyhow::{Context, Result};
use dioxus::{prelude::*, DomEdit, Mutations};
mod test_logging;
use dioxus::{prelude::*, DomEdit};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
mod test_logging;
use DomEdit::*;
const LOGGING_ENABLED: bool = false;
fn new_dom<P: Properties + 'static>(app: FC<P>, props: P) -> VirtualDom {
const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED);
VirtualDom::new_with_props(app, props)
}
#[test]
fn test_original_diff() {
static App: FC<()> = |cx| {
static APP: FC<()> = |cx| {
cx.render(rsx! {
div {
div {
@@ -21,7 +27,7 @@ fn test_original_diff() {
})
};
let mut dom = VirtualDom::new(App);
let mut dom = new_dom(APP, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
@@ -41,7 +47,7 @@ fn test_original_diff() {
#[async_std::test]
async fn create() {
static App: FC<()> = |cx| {
static APP: FC<()> = |cx| {
cx.render(rsx! {
div {
div {
@@ -59,9 +65,9 @@ async fn create() {
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(APP, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
[
@@ -92,7 +98,7 @@ async fn create() {
#[async_std::test]
async fn create_list() {
static App: FC<()> = |cx| {
static APP: FC<()> = |cx| {
cx.render(rsx! {
{(0..3).map(|f| rsx!{ div {
"hello"
@@ -100,10 +106,8 @@ async fn create_list() {
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(APP, ());
let mutations = dom.rebuild();
// copilot wrote this test :P
assert_eq!(
@@ -134,7 +138,7 @@ async fn create_list() {
#[async_std::test]
async fn create_simple() {
static App: FC<()> = |cx| {
static APP: FC<()> = |cx| {
cx.render(rsx! {
div {}
div {}
@@ -143,10 +147,8 @@ async fn create_simple() {
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(APP, ());
let mutations = dom.rebuild();
// copilot wrote this test :P
assert_eq!(
@@ -179,10 +181,8 @@ async fn create_components() {
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(App, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
@@ -225,10 +225,8 @@ async fn anchors() {
})
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(App, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
[
@@ -247,14 +245,18 @@ async fn anchors() {
#[async_std::test]
async fn suspended() {
static App: FC<()> = |cx| {
let val = use_suspense(cx, || async {}, |cx, _| cx.render(rsx! { "hi "}));
let val = use_suspense(
cx,
|| async {
//
},
|cx, _| cx.render(rsx! { "hi "}),
);
cx.render(rsx! { {val} })
};
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
let mut dom = new_dom(App, ());
let mutations = dom.rebuild();
assert_eq!(
mutations.edits,
-2
View File
@@ -1,2 +0,0 @@
/// A virtualdom wrapper used for testing purposes.
pub struct DebugDiff {}
-48
View File
@@ -1,48 +0,0 @@
//! tests to prove that the iterative implementation works
use dioxus::prelude::*;
mod test_logging;
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
const LOGGING_ENABLED: bool = false;
#[async_std::test]
async fn test_iterative_create_components() {
static App: FC<()> = |cx| {
// test root fragments
cx.render(rsx! {
Child { "abc1" }
Child { "abc2" }
Child { "abc3" }
})
};
fn Child(cx: Context<()>) -> DomTree {
// test root fragments, anchors, and ChildNode type
cx.render(rsx! {
h1 {}
div { {cx.children()} }
Fragment {
Fragment {
Fragment {
"wozza"
}
}
}
{(0..0).map(|_f| rsx!{ div { "walalla"}})}
p {}
})
}
test_logging::set_up_logging(LOGGING_ENABLED);
let mut dom = VirtualDom::new(App);
let mutations = dom.rebuild_async().await;
dbg!(mutations);
let mutations = dom.diff();
dbg!(mutations);
}
+32 -19
View File
@@ -4,24 +4,19 @@
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::{prelude::*, DomEdit, TestDom};
use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
mod test_logging;
use DomEdit::*;
// logging is wired up to the test harness
// feel free to enable while debugging
const IS_LOGGING_ENABLED: bool = false;
fn new_dom() -> TestDom {
const IS_LOGGING_ENABLED: bool = false;
test_logging::set_up_logging(IS_LOGGING_ENABLED);
TestDom::new()
}
#[test]
fn diffing_works() {}
use DomEdit::*;
/// Should push the text node onto the stack and modify it
#[test]
@@ -98,8 +93,8 @@ fn fragments_create_properly() {
fn empty_fragments_create_anchors() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|f| rsx! { div {}}) });
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) });
let (create, change) = dom.lazy_diff(left, right);
@@ -121,8 +116,8 @@ fn empty_fragments_create_anchors() {
fn empty_fragments_create_many_anchors() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|f| rsx! { div {}}) });
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) });
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
@@ -148,7 +143,7 @@ fn empty_fragments_create_many_anchors() {
fn empty_fragments_create_anchors_with_many_children() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let left = rsx!({ (0..0).map(|_| rsx! { div {} }) });
let right = rsx!({
(0..3).map(|f| {
rsx! { div { "hello: {f}" }}
@@ -192,11 +187,11 @@ fn many_items_become_fragment() {
let dom = new_dom();
let left = rsx!({
(0..2).map(|f| {
(0..2).map(|_| {
rsx! { div { "hello" }}
})
});
let right = rsx!({ (0..0).map(|f| rsx! { div {} }) });
let right = rsx!({ (0..0).map(|_| rsx! { div {} }) });
let (create, change) = dom.lazy_diff(left, right);
assert_eq!(
@@ -235,17 +230,17 @@ fn two_equal_fragments_are_equal() {
let dom = new_dom();
let left = rsx!({
(0..2).map(|f| {
(0..2).map(|_| {
rsx! { div { "hello" }}
})
});
let right = rsx!({
(0..2).map(|f| {
(0..2).map(|_| {
rsx! { div { "hello" }}
})
});
let (create, change) = dom.lazy_diff(left, right);
let (_create, change) = dom.lazy_diff(left, right);
assert!(change.edits.is_empty());
}
@@ -263,7 +258,7 @@ fn two_fragments_with_differrent_elements_are_differet() {
p {}
);
let (create, changes) = dom.lazy_diff(left, right);
let (_create, changes) = dom.lazy_diff(left, right);
log::debug!("{:#?}", &changes);
assert_eq!(
changes.edits,
@@ -700,3 +695,21 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
]
);
}
#[test]
fn suspense() {
let dom = new_dom();
let edits = dom.create(LazyNodes::new(|f| {
use std::cell::{Cell, RefCell};
VNode::Suspended(f.bump().alloc(VSuspended {
task_id: 0,
callback: RefCell::new(None),
dom_id: Cell::new(None),
}))
}));
assert_eq!(
edits.edits,
[CreatePlaceholder { id: 0 }, AppendChildren { many: 1 }]
);
}
-6
View File
@@ -1,6 +0,0 @@
// type VirtualNode = VNode;
/// Test a basic usage of a virtual dom + text renderer combo
#[test]
fn simple_integration() {}
+1
View File
@@ -0,0 +1 @@
//! Tests for the lifecycle of components.
+1
View File
@@ -0,0 +1 @@
//! Tests for the scheduler.
-30
View File
@@ -1,30 +0,0 @@
//! Diffing is interruptible, but uses yield_now which is loop-pollable
//!
//! This means you can actually call it synchronously if you want.
use anyhow::{Context, Result};
use dioxus::{prelude::*, scope::Scope};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
use futures_util::FutureExt;
#[test]
fn worksync() {
static App: FC<()> = |cx| {
cx.render(rsx! {
div {"hello"}
})
};
let mut dom = VirtualDom::new(App);
let mut fut = dom.rebuild_async().boxed_local();
let mutations = loop {
let g = (&mut fut).now_or_never();
if g.is_some() {
break g.unwrap();
}
};
dbg!(mutations);
}
-4
View File
@@ -65,10 +65,6 @@ features = [
"HtmlOptionElement",
]
[profile.release]
lto = true
opt-level = 's'
[lib]
crate-type = ["cdylib", "rlib"]
+79 -79
View File
@@ -582,85 +582,85 @@ pub fn load_document() -> Document {
pub fn event_name_from_typ(typ: &str) -> &'static str {
match typ {
"copy" => "oncopy",
"cut" => "oncut",
"paste" => "onpaste",
"compositionend" => "oncompositionend",
"compositionstart" => "oncompositionstart",
"compositionupdate" => "oncompositionupdate",
"keydown" => "onkeydown",
"keypress" => "onkeypress",
"keyup" => "onkeyup",
"focus" => "onfocus",
"blur" => "onblur",
"change" => "onchange",
"input" => "oninput",
"invalid" => "oninvalid",
"reset" => "onreset",
"submit" => "onsubmit",
"click" => "onclick",
"contextmenu" => "oncontextmenu",
"doubleclick" => "ondoubleclick",
"drag" => "ondrag",
"dragend" => "ondragend",
"dragenter" => "ondragenter",
"dragexit" => "ondragexit",
"dragleave" => "ondragleave",
"dragover" => "ondragover",
"dragstart" => "ondragstart",
"drop" => "ondrop",
"mousedown" => "onmousedown",
"mouseenter" => "onmouseenter",
"mouseleave" => "onmouseleave",
"mousemove" => "onmousemove",
"mouseout" => "onmouseout",
"mouseover" => "onmouseover",
"mouseup" => "onmouseup",
"pointerdown" => "onpointerdown",
"pointermove" => "onpointermove",
"pointerup" => "onpointerup",
"pointercancel" => "onpointercancel",
"gotpointercapture" => "ongotpointercapture",
"lostpointercapture" => "onlostpointercapture",
"pointerenter" => "onpointerenter",
"pointerleave" => "onpointerleave",
"pointerover" => "onpointerover",
"pointerout" => "onpointerout",
"select" => "onselect",
"touchcancel" => "ontouchcancel",
"touchend" => "ontouchend",
"touchmove" => "ontouchmove",
"touchstart" => "ontouchstart",
"scroll" => "onscroll",
"wheel" => "onwheel",
"animationstart" => "onanimationstart",
"animationend" => "onanimationend",
"animationiteration" => "onanimationiteration",
"transitionend" => "ontransitionend",
"abort" => "onabort",
"canplay" => "oncanplay",
"canplaythrough" => "oncanplaythrough",
"durationchange" => "ondurationchange",
"emptied" => "onemptied",
"encrypted" => "onencrypted",
"ended" => "onended",
"error" => "onerror",
"loadeddata" => "onloadeddata",
"loadedmetadata" => "onloadedmetadata",
"loadstart" => "onloadstart",
"pause" => "onpause",
"play" => "onplay",
"playing" => "onplaying",
"progress" => "onprogress",
"ratechange" => "onratechange",
"seeked" => "onseeked",
"seeking" => "onseeking",
"stalled" => "onstalled",
"suspend" => "onsuspend",
"timeupdate" => "ontimeupdate",
"volumechange" => "onvolumechange",
"waiting" => "onwaiting",
"toggle" => "ontoggle",
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
_ => {
panic!("unsupported event type")
}