From 4a31b29703071f9a860c342cf3819e83e58f8f7a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 1 Nov 2022 18:42:29 -0700 Subject: [PATCH] feat: simple tests passing --- Cargo.toml | 2 +- packages/core-macro/src/lib.rs | 3 +- packages/core/src.old/scopes.rs | 4 - packages/core/src/any_props.rs | 33 ++- packages/core/src/arena.rs | 40 +-- packages/core/src/bump_frame.rs | 4 +- packages/core/src/component.rs | 40 ++- packages/core/src/create.rs | 140 +++++++--- packages/core/src/diff.rs | 19 +- packages/core/src/element.rs | 4 +- packages/core/src/events.rs | 159 +++++++++++ packages/core/src/factory.rs | 46 ++++ packages/core/src/future_container.rs | 65 +++++ packages/core/src/garbage.rs | 30 +++ packages/core/src/lazynodes.rs | 324 ++++++++++++++++++++++ packages/core/src/lib.rs | 189 +++++++++---- packages/core/src/mutations.rs | 23 +- packages/core/src/nodes.rs | 23 +- packages/core/src/scope_arena.rs | 51 ++-- packages/core/src/scopes.rs | 369 ++++++++++++++++++++++++-- packages/core/src/virtualdom.rs | 91 +++++++ packages/core/tests/interpreter.rs | 114 -------- packages/core/tests/simple_syntax.rs | 26 ++ packages/dioxus/Cargo.toml | 2 +- packages/dioxus/tests/rsx_syntax.rs | 28 +- packages/html/src/events.rs | 35 +-- packages/rsx/src/lib.rs | 79 +++--- 27 files changed, 1568 insertions(+), 375 deletions(-) create mode 100644 packages/core/src/events.rs create mode 100644 packages/core/src/factory.rs create mode 100644 packages/core/src/future_container.rs create mode 100644 packages/core/src/garbage.rs create mode 100644 packages/core/src/lazynodes.rs create mode 100644 packages/core/src/virtualdom.rs delete mode 100644 packages/core/tests/interpreter.rs create mode 100644 packages/core/tests/simple_syntax.rs diff --git a/Cargo.toml b/Cargo.toml index 370b60156..a523e1fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ "packages/rsx", "packages/native-core", "packages/native-core-macro", - "packages/edit-stream", + # "packages/edit-stream", "docs/guide", ] diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index de0155e14..e1c445262 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -11,8 +11,7 @@ use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use rsx::*; - let item = parse_macro_input!(input as IfmtInput); - format_args_f_impl(item) + format_args_f_impl(parse_macro_input!(input as IfmtInput)) .unwrap_or_else(|err| err.to_compile_error()) .into() } diff --git a/packages/core/src.old/scopes.rs b/packages/core/src.old/scopes.rs index be14cc4b2..24550bc38 100644 --- a/packages/core/src.old/scopes.rs +++ b/packages/core/src.old/scopes.rs @@ -424,7 +424,6 @@ impl

Clone for Scope<'_, P> { } impl<'a, P> std::ops::Deref for Scope<'a, P> { - // rust will auto deref again to the original 'a lifetime at the call site type Target = &'a ScopeState; fn deref(&self) -> &Self::Target { &self.scope @@ -621,9 +620,6 @@ impl ScopeState { Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) } - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. pub fn needs_update(&self) { self.needs_update_any(self.scope_id()); } diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index e7828ed0c..199d5cc5b 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,7 +1,9 @@ use std::cell::Cell; +use futures_util::Future; + use crate::{ - component::Component, + component::{Component, ComponentFn, Dummy, IntoComponent}, element::Element, scopes::{Scope, ScopeState}, }; @@ -12,21 +14,18 @@ pub trait AnyProps { unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; } -pub(crate) struct VComponentProps

{ - pub render_fn: Component

, +pub(crate) struct VComponentProps = Dummy> { + pub render_fn: ComponentFn, pub memo: unsafe fn(&P, &P) -> bool, - pub props: Scope

, + pub props: *const P, } impl VComponentProps<()> { pub fn new_empty(render_fn: Component<()>) -> Self { Self { - render_fn, + render_fn: render_fn.into_component(), memo: <() as PartialEq>::eq, - props: Scope { - props: (), - state: Cell::new(std::ptr::null_mut()), - }, + props: std::ptr::null_mut(), } } } @@ -35,10 +34,10 @@ impl

VComponentProps

{ pub(crate) fn new( render_fn: Component

, memo: unsafe fn(&P, &P) -> bool, - props: Scope

, + props: *const P, ) -> Self { Self { - render_fn, + render_fn: render_fn.into_component(), memo, props, } @@ -62,10 +61,18 @@ impl

AnyProps for VComponentProps

{ fn render<'a>(&'a self, scope: &'a ScopeState) -> Element<'a> { // Make sure the scope ptr is not null - self.props.state.set(scope); + // self.props.state.set(scope); + + let scope = Scope { + props: unsafe { &*self.props }, + scope, + }; // Call the render function directly // todo: implement async - (self.render_fn)(unsafe { std::mem::transmute(&self.props) }) + match self.render_fn { + ComponentFn::Sync(f) => f(scope), + ComponentFn::Async(_) => todo!(), + } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 231b554ad..02b9ad994 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,30 +1,32 @@ use std::num::NonZeroUsize; +use crate::{ + nodes::{Template, VNode}, + virtualdom::VirtualDom, +}; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -// pub struct ElementId(pub NonZeroUsize); - -// impl Default for ElementId { -// fn default() -> Self { -// Self(NonZeroUsize::new(1).unwrap()) -// } -// } - -pub struct ElementArena { - counter: usize, +pub struct ElementPath { + pub template: *mut VNode<'static>, + pub element: usize, } -impl Default for ElementArena { - fn default() -> Self { - Self { counter: 1 } - } -} +impl VirtualDom { + pub fn next_element(&mut self, template: &VNode) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + + entry.insert(ElementPath { + template: template as *const _ as *mut _, + element: id, + }); -impl ElementArena { - pub fn next(&mut self) -> ElementId { - let id = self.counter; - self.counter += 1; ElementId(id) } + + pub fn cleanup_element(&mut self, id: ElementId) { + self.elements.remove(id.0); + } } diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index 57ddbdc3a..8b3f3b86b 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -2,11 +2,11 @@ use std::cell::Cell; use bumpalo::Bump; -use crate::nodes::VTemplate; +use crate::nodes::VNode; pub struct BumpFrame { pub bump: Bump, - pub node: Cell<*const VTemplate<'static>>, + pub node: Cell<*const VNode<'static>>, } impl BumpFrame { pub fn new(capacity: usize) -> Self { diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 53825a33c..153a562e3 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -2,6 +2,44 @@ // fn into_component_type(self) -> ComponentType; // } +use futures_util::Future; + use crate::{element::Element, scopes::Scope}; -pub type Component = fn(&Scope) -> Element; +pub type Component = fn(Scope) -> Element; + +pub enum ComponentFn = Dummy> { + Sync(fn(Scope) -> Element), + Async(fn(Scope) -> F), +} + +pub trait IntoComponent { + fn into_component(self) -> ComponentFn; +} + +impl IntoComponent for fn(Scope) -> Element { + fn into_component(self) -> ComponentFn { + ComponentFn::Sync(self) + } +} + +pub struct AsyncMarker; +impl<'a, T, F: Future>> IntoComponent + for fn(&'a Scope) -> F +{ + fn into_component(self) -> ComponentFn { + todo!() + } +} + +pub struct Dummy; +impl Future for Dummy { + type Output = (); + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + unreachable!() + } +} diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 5a25808a0..f7b8744d6 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,33 +1,30 @@ -use crate::VirtualDom; - -use crate::any_props::VComponentProps; -use crate::arena::ElementArena; -use crate::component::Component; use crate::mutations::Mutation; -use crate::nodes::{ - AttributeLocation, DynamicNode, DynamicNodeKind, Template, TemplateId, TemplateNode, -}; -use crate::scopes::Scope; -use crate::{ - any_props::AnyProps, - arena::ElementId, - bump_frame::BumpFrame, - nodes::VTemplate, - scopes::{ComponentPtr, ScopeId, ScopeState}, -}; -use slab::Slab; +use crate::mutations::Mutation::*; +use crate::nodes::VNode; +use crate::nodes::{DynamicNode, DynamicNodeKind, TemplateNode}; +use crate::virtualdom::VirtualDom; impl VirtualDom { /// Create this template and write its mutations pub fn create<'a>( &mut self, mutations: &mut Vec>, - template: &'a VTemplate<'a>, + template: &'a VNode<'a>, ) -> usize { - // The best renderers will have tempaltes prehydrated + // The best renderers will have templates prehydrated // Just in case, let's create the template using instructions anyways if !self.templates.contains_key(&template.template.id) { - self.create_static_template(mutations, template.template); + for node in template.template.roots { + self.create_static_node(mutations, template, node); + } + + mutations.push(SaveTemplate { + name: template.template.id, + m: template.template.roots.len(), + }); + + self.templates + .insert(template.template.id, template.template.clone()); } // Walk the roots backwards, creating nodes and assigning IDs @@ -38,28 +35,37 @@ impl VirtualDom { let mut on_stack = 0; for (root_idx, root) in template.template.roots.iter().enumerate() { on_stack += match root { + TemplateNode::Element { .. } | TemplateNode::Text(_) => { + mutations.push(LoadTemplate { + name: template.template.id, + index: root_idx, + }); + + 1 + } + TemplateNode::Dynamic(id) => { self.create_dynamic_node(mutations, template, &template.dynamic_nodes[*id]) } - TemplateNode::DynamicText { .. } - | TemplateNode::Element { .. } - | TemplateNode::Text(_) => 1, + + TemplateNode::DynamicText { .. } => 1, }; // we're on top of a node that has a dynamic attribute for a descendant // Set that attribute now before the stack gets in a weird state while let Some(loc) = dynamic_attrs.next_if(|a| a.path[0] == root_idx as u8) { // Attach all the elementIDs to the nodes with dynamic content - let id = self.elements.next(); - mutations.push(Mutation::AssignId { + let id = self.next_element(template); + + mutations.push(AssignId { path: &loc.path[1..], id, }); loc.mounted_element.set(id); - for attr in loc.attrs { - mutations.push(Mutation::SetAttribute { + for attr in loc.attrs.iter() { + mutations.push(SetAttribute { name: attr.name, value: attr.value, id, @@ -67,31 +73,71 @@ impl VirtualDom { } } - // We're on top of a node that has a dynamic child for a descndent - // Might as well set it now while we can + // We're on top of a node that has a dynamic child for a descendant while let Some(node) = dynamic_nodes.next_if(|f| f.path[0] == root_idx as u8) { - self.create_dynamic_node(mutations, template, node); + let m = self.create_dynamic_node(mutations, template, node); + mutations.push(ReplacePlaceholder { + m, + path: &node.path[1..], + }); } } on_stack } - fn create_static_template(&mut self, mutations: &mut Vec, template: &Template) { - todo!("load template") + fn create_static_node<'a>( + &mut self, + mutations: &mut Vec>, + template: &'a VNode<'a>, + node: &'a TemplateNode<'static>, + ) { + match *node { + TemplateNode::Dynamic(_) => mutations.push(CreatePlaceholder), + TemplateNode::Text(value) => mutations.push(CreateText { value }), + TemplateNode::DynamicText { .. } => mutations.push(CreateText { + value: "placeholder", + }), + TemplateNode::Element { + attrs, + children, + namespace, + tag, + } => { + let id = self.next_element(template); + + mutations.push(CreateElement { + name: tag, + namespace, + id, + }); + + mutations.extend(attrs.into_iter().map(|attr| SetAttribute { + name: attr.name, + value: attr.value, + id, + })); + + children + .into_iter() + .for_each(|child| self.create_static_node(mutations, template, child)); + + mutations.push(AppendChildren { m: children.len() }) + } + } } fn create_dynamic_node<'a>( &mut self, mutations: &mut Vec>, - template: &VTemplate<'a>, + template: &'a VNode<'a>, node: &'a DynamicNode<'a>, ) -> usize { match &node.kind { DynamicNodeKind::Text { id, value } => { - let new_id = self.elements.next(); + let new_id = self.next_element(template); id.set(new_id); - mutations.push(Mutation::HydrateText { + mutations.push(HydrateText { id: new_id, path: &node.path[1..], value, @@ -99,16 +145,28 @@ impl VirtualDom { 1 } - DynamicNodeKind::Component { props, fn_ptr, .. } => { - let id = self.new_scope(*fn_ptr, None, ElementId(0), *props); + + DynamicNodeKind::Component { props, .. } => { + let id = self.new_scope(*props); let template = self.run_scope(id); - todo!("create component has bad data"); + // shut up about lifetimes please, I know what I'm doing + let template: &VNode = unsafe { std::mem::transmute(template) }; + + self.scope_stack.push(id); + let created = self.create(mutations, template); + self.scope_stack.pop(); + + created + } + + DynamicNodeKind::Fragment { children } => { + // + children + .iter() + .fold(0, |acc, child| acc + self.create(mutations, child)) } - DynamicNodeKind::Fragment { children } => children - .iter() - .fold(0, |acc, child| acc + self.create(mutations, child)), } } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 7938e4934..43b454c8b 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,7 @@ -use crate::VirtualDom; +use crate::virtualdom::VirtualDom; use crate::any_props::VComponentProps; -use crate::arena::ElementArena; + use crate::component::Component; use crate::mutations::Mutation; use crate::nodes::{DynamicNode, Template, TemplateId}; @@ -10,9 +10,18 @@ use crate::{ any_props::AnyProps, arena::ElementId, bump_frame::BumpFrame, - nodes::VTemplate, - scopes::{ComponentPtr, ScopeId, ScopeState}, + nodes::VNode, + scopes::{ScopeId, ScopeState}, }; use slab::Slab; -impl VirtualDom {} +pub struct DirtyScope { + height: usize, + id: ScopeId, +} + +impl VirtualDom { + fn diff_scope<'a>(&'a mut self, mutations: &mut Vec>, scope: ScopeId) { + let scope_state = &mut self.scopes[scope.0]; + } +} diff --git a/packages/core/src/element.rs b/packages/core/src/element.rs index 63408f074..33764e126 100644 --- a/packages/core/src/element.rs +++ b/packages/core/src/element.rs @@ -1,3 +1,3 @@ -use crate::nodes::{Template, VTemplate}; +use crate::nodes::{Template, VNode}; -pub type Element<'a> = Option>; +pub type Element<'a> = Option>; diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs new file mode 100644 index 000000000..3b7ebf4a1 --- /dev/null +++ b/packages/core/src/events.rs @@ -0,0 +1,159 @@ +use std::{any::Any, cell::Cell}; + +use crate::{arena::ElementId, nodes::Listener, scopes::ScopeId, virtualdom::VirtualDom}; + +/// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel. +/// +/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree +/// where each listener is checked and fired if the event name matches. +/// +/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in +/// attempting to downcast the event data. +/// +/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but +/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel. +/// +/// # Example +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// render!(div { +/// onclick: move |_| println!("Clicked!") +/// }) +/// } +/// +/// let mut dom = VirtualDom::new(App); +/// let mut scheduler = dom.get_scheduler_channel(); +/// scheduler.unbounded_send(SchedulerMsg::UiEvent( +/// UserEvent { +/// scope_id: None, +/// priority: EventPriority::Medium, +/// name: "click", +/// element: Some(ElementId(0)), +/// data: Arc::new(ClickEvent { .. }) +/// } +/// )).unwrap(); +/// ``` +#[derive(Debug)] +pub struct UiEvent { + /// The priority of the event to be scheduled around ongoing work + pub priority: EventPriority, + + /// The optional real node associated with the trigger + pub element: ElementId, + + /// The event type IE "onclick" or "onmouseover" + pub name: &'static str, + pub bubble: Cell, + pub event: T, +} + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - `NoPriority` = 0 +/// - `LowPriority` = 1 +/// - `NormalPriority` = 2 +/// - `UserBlocking` = 3 +/// - `HighPriority` = 4 +/// - `ImmediatePriority` = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// Work that must be completed during the EventHandler phase. + /// + /// Currently this is reserved for controlled inputs. + Immediate = 3, + + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take precedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (Suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} + +impl VirtualDom { + /// Returns None if no element could be found + pub fn handle_event(&mut self, event: &UiEvent) -> Option<()> { + let path = self.elements.get(event.element.0)?; + + let location = unsafe { &mut *path.template } + .dynamic_attrs + .iter_mut() + .position(|attr| attr.mounted_element.get() == event.element)?; + + let mut index = Some((path.template, location)); + + let mut listeners = Vec::<&mut Listener>::new(); + + while let Some((raw_parent, dyn_index)) = index { + let parent = unsafe { &mut *raw_parent }; + let path = parent.dynamic_nodes[dyn_index].path; + + listeners.extend( + parent + .dynamic_attrs + .iter_mut() + .filter(|attr| is_path_ascendant(attr.path, path)) + .map(|f| f.listeners.iter_mut().filter(|f| f.name == event.name)) + .flatten(), + ); + + index = parent.parent; + } + + for listener in listeners { + (listener.callback)(&event.event); + } + + Some(()) + } +} + +// ensures a strict descendant relationship +// returns false if the paths are equal +fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool { + if small.len() >= big.len() { + return false; + } + + small == &big[..small.len()] +} + +#[test] +fn matches_slice() { + let left = &[1, 2, 3]; + let right = &[1, 2, 3, 4, 5]; + assert!(is_path_ascendant(left, right)); + assert!(!is_path_ascendant(right, left)); + assert!(!is_path_ascendant(left, left)); + + assert!(is_path_ascendant(&[1, 2], &[1, 2, 3, 4, 5])); +} diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs new file mode 100644 index 000000000..695a6ccaf --- /dev/null +++ b/packages/core/src/factory.rs @@ -0,0 +1,46 @@ +use std::fmt::Arguments; + +use crate::{innerlude::DynamicNode, LazyNodes, ScopeState, VNode}; + +impl ScopeState { + /// Create some text that's allocated along with the other vnodes + /// + pub fn text(&self, args: Arguments) -> DynamicNode { + // let (text, _is_static) = self.raw_text(args); + + // VNode::Text(self.bump.alloc(VText { + // text, + // id: Default::default(), + // })) + + todo!() + } + + pub fn fragment_from_iter<'a, I, F: IntoVnode<'a, I>>( + &'a self, + it: impl IntoIterator, + ) -> DynamicNode { + let mut bump_vec = bumpalo::vec![in self.bump();]; + + for item in it { + bump_vec.push(item.into_dynamic_node(self)); + } + + DynamicNode { + path: &[0, 0], + kind: crate::innerlude::DynamicNodeKind::Fragment { + children: bump_vec.into_bump_slice(), + }, + } + } +} + +pub trait IntoVnode<'a, A = ()> { + fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a>; +} + +impl<'a, 'b> IntoVnode<'a> for LazyNodes<'a, 'b> { + fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a> { + self.call(cx) + } +} diff --git a/packages/core/src/future_container.rs b/packages/core/src/future_container.rs new file mode 100644 index 000000000..1d88d0d82 --- /dev/null +++ b/packages/core/src/future_container.rs @@ -0,0 +1,65 @@ +use futures_channel::mpsc::UnboundedSender; +use futures_util::Future; +use std::{cell::RefCell, rc::Rc}; + +use crate::innerlude::ScopeId; +/// The type of message that can be sent to the scheduler. +/// +/// These messages control how the scheduler will process updates to the UI. +#[derive(Debug)] +pub enum SchedulerMsg { + /// Events from athe Renderer + Event, + + /// Immediate updates from Components that mark them as dirty + Immediate(ScopeId), + + /// Mark all components as dirty and update them + DirtyAll, + + /// New tasks from components that should be polled when the next poll is ready + NewTask(ScopeId), +} + +// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked +#[derive(Clone)] +pub struct FutureQueue { + pub sender: UnboundedSender, + pub queue: Rc>>>>, +} + +impl FutureQueue { + pub fn new(sender: UnboundedSender) -> Self { + Self { + sender, + queue: Default::default(), + } + } + + pub fn spawn(&self, id: ScopeId, fut: impl Future + 'static) -> TaskId { + self.sender + .unbounded_send(SchedulerMsg::NewTask(id)) + .unwrap(); + self.queue.borrow_mut().push(Box::new(fut)); + + todo!() + } + + pub fn remove(&self, id: TaskId) { + todo!() + } +} + +/// A task's unique identifier. +/// +/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused +/// once a Task has been completed. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct TaskId { + /// The global ID of the task + pub id: usize, + + /// The original scope that this task was scheduled in + pub scope: ScopeId, +} diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs new file mode 100644 index 000000000..ccaf7c253 --- /dev/null +++ b/packages/core/src/garbage.rs @@ -0,0 +1,30 @@ +use crate::{ + nodes::{DynamicNodeKind, VNode}, + scopes::ScopeId, + virtualdom::VirtualDom, +}; + +impl VirtualDom { + pub fn drop_scope(&mut self, id: ScopeId) { + let scope = self.scopes.get(id.0).unwrap(); + + let root = scope.root_node(); + let root = unsafe { std::mem::transmute(root) }; + + self.drop_template(root); + } + + pub fn drop_template<'a>(&'a mut self, template: &'a VNode<'a>) { + for node in template.dynamic_nodes.iter() { + match &node.kind { + DynamicNodeKind::Text { id, .. } => {} + + DynamicNodeKind::Component { .. } => { + todo!() + } + + DynamicNodeKind::Fragment { children } => {} + } + } + } +} diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs new file mode 100644 index 000000000..76c25641f --- /dev/null +++ b/packages/core/src/lazynodes.rs @@ -0,0 +1,324 @@ +//! Support for storing lazy-nodes on the stack +//! +//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls +//! to `rsx!` more efficient. +//! +//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures. +//! +//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case, +//! we build a tiny alloactor in the stack and allocate the closure into that. +//! +//! The logic for this was borrowed from . Unfortunately, this crate does not +//! support non-static closures, so we've implemented the core logic of `ValueA` in this module. + +use crate::{innerlude::VNode, ScopeState}; +use std::mem; + +/// A concrete type provider for closures that build [`VNode`] structures. +/// +/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over +/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of [`IntoVNode`]. +/// +/// +/// ```rust, ignore +/// LazyNodes::new(|f| f.element("div", [], [], [] None)) +/// ``` +pub struct LazyNodes<'a, 'b> { + inner: StackNodeStorage<'a, 'b>, +} + +pub type NodeFactory<'a> = &'a ScopeState; + +type StackHeapSize = [usize; 16]; + +enum StackNodeStorage<'a, 'b> { + Stack(LazyStack), + Heap(Box>) -> Option> + 'b>), +} + +impl<'a, 'b> LazyNodes<'a, 'b> { + /// Create a new [`LazyNodes`] closure, optimistically placing it onto the stack. + /// + /// If the closure cannot fit into the stack allocation (16 bytes), then it + /// is placed on the heap. Most closures will fit into the stack, and is + /// the most optimal way to use the creation function. + pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { + // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch + let mut slot = Some(val); + + let val = move |fac: Option>| { + fac.map( + slot.take() + .expect("LazyNodes closure to be called only once"), + ) + }; + + // miri does not know how to work with mucking directly into bytes + // just use a heap allocated type when miri is running + if cfg!(miri) { + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } + } else { + unsafe { LazyNodes::new_inner(val) } + } + } + + /// Create a new [`LazyNodes`] closure, but force it onto the heap. + pub fn new_boxed(inner: F) -> Self + where + F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, + { + // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch + let mut slot = Some(inner); + + Self { + inner: StackNodeStorage::Heap(Box::new(move |fac: Option>| { + fac.map( + slot.take() + .expect("LazyNodes closure to be called only once"), + ) + })), + } + } + + unsafe fn new_inner(val: F) -> Self + where + F: FnMut(Option>) -> Option> + 'b, + { + let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; + + assert_eq!( + ptr as *const u8, &val as *const _ as *const u8, + "MISUSE: Closure returned different pointer" + ); + assert_eq!( + std::mem::size_of_val(&*ptr), + std::mem::size_of::(), + "MISUSE: Closure returned a subset pointer" + ); + + let words = ptr_as_slice(&mut ptr); + assert!( + words[0] == &val as *const _ as usize, + "BUG: Pointer layout is not (data_ptr, info...)" + ); + + // - Ensure that Self is aligned same as data requires + assert!( + std::mem::align_of::() <= std::mem::align_of::(), + "TODO: Enforce alignment >{} (requires {})", + std::mem::align_of::(), + std::mem::align_of::() + ); + + let info = &words[1..]; + let data = words[0] as *mut (); + let size = mem::size_of::(); + + let stored_size = info.len() * mem::size_of::() + size; + let max_size = mem::size_of::(); + + if stored_size > max_size { + Self { + inner: StackNodeStorage::Heap(Box::new(val)), + } + } else { + let mut buf: StackHeapSize = StackHeapSize::default(); + + assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); + + // Place pointer information at the end of the region + // - Allows the data to be at the start for alignment purposes + { + let info_ofs = buf.as_ref().len() - info.len(); + let info_dst = &mut buf.as_mut()[info_ofs..]; + for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { + *d = *v; + } + } + + let src_ptr = data as *const u8; + let dataptr = buf.as_mut_ptr().cast::(); + + for i in 0..size { + *dataptr.add(i) = *src_ptr.add(i); + } + + std::mem::forget(val); + + Self { + inner: StackNodeStorage::Stack(LazyStack { + _align: [], + buf, + dropped: false, + }), + } + } + } + + /// Call the closure with the given factory to produce real [`VNode`]. + /// + /// ```rust, ignore + /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); + /// + /// let fac = NodeFactory::new(&cx); + /// + /// let node = f.call(cac); + /// ``` + #[must_use] + pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { + match self.inner { + StackNodeStorage::Heap(mut lazy) => { + lazy(Some(f)).expect("Closure should not be called twice") + } + StackNodeStorage::Stack(mut stack) => stack.call(f), + } + } +} + +struct LazyStack { + _align: [u64; 0], + buf: StackHeapSize, + dropped: bool, +} + +impl LazyStack { + fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { + let LazyStack { buf, .. } = self; + let data = buf.as_ref(); + + let info_size = + mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() + / mem::size_of::() + - 1; + + let info_ofs = data.len() - info_size; + + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; + + self.dropped = true; + + let clos = unsafe { &mut *g }; + clos(Some(f)).unwrap() + } +} +impl Drop for LazyStack { + fn drop(&mut self) { + if !self.dropped { + let LazyStack { buf, .. } = self; + let data = buf.as_ref(); + + let info_size = mem::size_of::< + *mut dyn FnMut(Option>) -> Option>, + >() / mem::size_of::() + - 1; + + let info_ofs = data.len() - info_size; + + let g: *mut dyn FnMut(Option>) -> Option> = + unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; + + self.dropped = true; + + let clos = unsafe { &mut *g }; + clos(None); + } + } +} + +/// Obtain mutable access to a pointer's words +fn ptr_as_slice(ptr: &mut T) -> &mut [usize] { + assert!(mem::size_of::() % mem::size_of::() == 0); + let words = mem::size_of::() / mem::size_of::(); + // SAFE: Points to valid memory (a raw pointer) + unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) } +} + +/// Re-construct a fat pointer +unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut T { + let mut rv = mem::MaybeUninit::<*mut T>::uninit(); + { + let s = ptr_as_slice(&mut rv); + s[0] = data_ptr; + s[1..].copy_from_slice(meta_vals); + } + let rv = rv.assume_init(); + assert_eq!(rv as *const (), data_ptr as *const ()); + rv +} + +fn round_to_words(len: usize) -> usize { + (len + mem::size_of::() - 1) / mem::size_of::() +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::innerlude::{Element, Scope, VirtualDom}; + +// #[test] +// fn it_works() { +// fn app(cx: Scope<()>) -> Element { +// cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) +// } + +// let mut dom = VirtualDom::new(app); +// dom.rebuild(); + +// let g = dom.base_scope().root_node(); +// dbg!(g); +// } + +// #[test] +// fn it_drops() { +// use std::rc::Rc; + +// struct AppProps { +// inner: Rc, +// } + +// fn app(cx: Scope) -> Element { +// struct DropInner { +// id: i32, +// } +// impl Drop for DropInner { +// fn drop(&mut self) { +// eprintln!("dropping inner"); +// } +// } + +// let caller = { +// let it = (0..10).map(|i| { +// let val = cx.props.inner.clone(); +// LazyNodes::new(move |f| { +// eprintln!("hell closure"); +// let inner = DropInner { id: i }; +// f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) +// }) +// }); + +// LazyNodes::new(|f| { +// eprintln!("main closure"); +// f.fragment_from_iter(it) +// }) +// }; + +// cx.render(caller) +// } + +// let inner = Rc::new(0); +// let mut dom = VirtualDom::new_with_props( +// app, +// AppProps { +// inner: inner.clone(), +// }, +// ); +// dom.rebuild(); + +// drop(dom); + +// assert_eq!(Rc::strong_count(&inner), 1); +// } +// } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5de133e1e..a8c737858 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,20 +1,3 @@ -use std::collections::HashMap; - -use crate::{ - any_props::AnyProps, - arena::ElementId, - bump_frame::BumpFrame, - nodes::VTemplate, - scopes::{ComponentPtr, ScopeId, ScopeState}, -}; -use any_props::VComponentProps; -use arena::ElementArena; -use component::Component; -use mutations::Mutation; -use nodes::{DynamicNode, Template, TemplateId}; -use scopes::Scope; -use slab::Slab; - mod any_props; mod arena; mod bump_frame; @@ -22,54 +5,142 @@ mod component; mod create; mod diff; mod element; +mod events; +mod factory; +mod future_container; +mod garbage; +mod lazynodes; mod mutations; mod nodes; mod scope_arena; mod scopes; +mod virtualdom; -pub struct VirtualDom { - templates: HashMap, - elements: ElementArena, - scopes: Slab, - scope_stack: Vec, +pub(crate) mod innerlude { + pub use crate::element::Element; + pub use crate::events::*; + pub use crate::future_container::*; + pub use crate::mutations::*; + pub use crate::nodes::*; + // pub use crate::properties::*; + pub use crate::lazynodes::*; + pub use crate::scopes::*; + pub use crate::virtualdom::*; + + // /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. + // /// + // /// Any [`None`] [`Element`] will automatically be coerced into a placeholder [`VNode`] with the [`VNode::Placeholder`] variant. + // pub type Element<'a> = Option>; + + /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`]. + /// + /// Components can be used in other components with two syntax options: + /// - lowercase as a function call with named arguments (rust style) + /// - uppercase as an element (react style) + /// + /// ## Rust-Style + /// + /// ```rust, ignore + /// fn example(cx: Scope) -> Element { + /// // ... + /// } + /// + /// rsx!( + /// example() + /// ) + /// ``` + /// ## React-Style + /// ```rust, ignore + /// fn Example(cx: Scope) -> Element { + /// // ... + /// } + /// + /// rsx!( + /// Example {} + /// ) + /// ``` + pub type Component

= fn(Scope

) -> Element; + + /// A list of attributes + pub type Attributes<'a> = Option<&'a [Attribute<'a>]>; } -impl VirtualDom { - pub fn new(app: Component) -> Self { - let mut res = Self { - templates: Default::default(), - scopes: Slab::default(), - elements: ElementArena::default(), - scope_stack: Vec::new(), - }; +pub use crate::innerlude::{ + // AnyAttributeValue, AnyEvent, Attribute, AttributeValue, Component, Element, ElementId, + Attribute, + AttributeValue, + Element, + EventPriority, + LazyNodes, + Listener, + NodeFactory, + Scope, + ScopeId, + ScopeState, + TaskId, + Template, + TemplateAttribute, + TemplateNode, + UiEvent, + VNode, + VirtualDom, +}; +// EventHandler, EventPriority, IntoVNode, LazyNodes, Listener, NodeFactory, Properties, Renderer, +// SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, Template, TemplateAttribute, TemplateNode, +// UiEvent, UserEvent, VComponent, VElement, VNode, VTemplate, VText, VirtualDom, - res.new_scope( - app as _, - None, - ElementId(0), - Box::new(VComponentProps::new_empty(app)), - ); - - res - } - - fn root_scope(&self) -> &ScopeState { - todo!() - } - - /// Render the virtualdom, waiting for all suspended nodes to complete before moving on - /// - /// Forces a full render of the virtualdom from scratch. - /// - /// Use other methods to update the virtualdom incrementally. - pub fn render_all<'a>(&'a mut self, mutations: &mut Vec>) { - let root = self.root_scope(); - - let root_template = root.current_arena(); - - let root_node: &'a VTemplate = unsafe { &*root_template.node.get() }; - let root_node: &'a VTemplate<'a> = unsafe { std::mem::transmute(root_node) }; - - self.create(mutations, root_node); - } +/// The purpose of this module is to alleviate imports of many common types +/// +/// This includes types like [`Scope`], [`Element`], and [`Component`]. +pub mod prelude { + pub use crate::innerlude::{ + Attribute, Element, EventPriority, LazyNodes, Listener, NodeFactory, Scope, ScopeId, + ScopeState, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom, + }; +} + +pub mod exports { + //! Important dependencies that are used by the rest of the library + //! Feel free to just add the dependencies in your own Crates.toml + pub use bumpalo; + pub use futures_channel; +} + +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ```ignore +/// let (data) = use_ref(&cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} + +/// get the code location of the code that called this function +#[macro_export] +macro_rules! get_line_num { + () => { + concat!( + file!(), + ":", + line!(), + ":", + column!(), + ":", + env!("CARGO_MANIFEST_DIR") + ) + }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index ee6f78f1e..3a6819042 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -17,6 +17,11 @@ pub enum Mutation<'a> { index: usize, }, + SaveTemplate { + name: &'static str, + m: usize, + }, + HydrateText { path: &'static [u8], value: &'a str, @@ -30,7 +35,7 @@ pub enum Mutation<'a> { ReplacePlaceholder { path: &'static [u8], - id: ElementId, + m: usize, }, AssignId { @@ -42,4 +47,20 @@ pub enum Mutation<'a> { Replace { id: ElementId, }, + + CreateElement { + name: &'a str, + namespace: Option<&'a str>, + id: ElementId, + }, + + CreateText { + value: &'a str, + }, + + CreatePlaceholder, + + AppendChildren { + m: usize, + }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 88432774d..4e54c8d4b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,4 @@ -use crate::{any_props::AnyProps, arena::ElementId, scopes::ComponentPtr}; +use crate::{any_props::AnyProps, arena::ElementId}; use std::{ any::{Any, TypeId}, cell::Cell, @@ -8,11 +8,14 @@ use std::{ pub type TemplateId = &'static str; /// A reference to a template along with any context needed to hydrate it -pub struct VTemplate<'a> { +pub struct VNode<'a> { // The ID assigned for the root of this template pub node_id: Cell, - pub template: &'static Template, + // When rendered, this template will be linked to its parent + pub parent: Option<(*mut VNode<'static>, usize)>, + + pub template: Template, pub root_ids: &'a [Cell], @@ -25,7 +28,6 @@ pub struct VTemplate<'a> { #[derive(Debug, Clone, Copy)] pub struct Template { pub id: &'static str, - pub roots: &'static [TemplateNode<'static>], } @@ -54,8 +56,7 @@ pub enum DynamicNodeKind<'a> { // IE in caps or with underscores Component { name: &'static str, - fn_ptr: ComponentPtr, - props: Box, + props: *mut dyn AnyProps, }, // Comes in with string interpolation or from format_args, include_str, etc @@ -66,7 +67,7 @@ pub enum DynamicNodeKind<'a> { // Anything that's coming in as an iterator Fragment { - children: &'a [VTemplate<'a>], + children: &'a [VNode<'a>], }, } @@ -80,8 +81,8 @@ pub struct TemplateAttribute<'a> { pub struct AttributeLocation<'a> { pub mounted_element: Cell, - pub attrs: &'a [Attribute<'a>], - pub listeners: &'a [Listener<'a>], + pub attrs: &'a mut [Attribute<'a>], + pub listeners: &'a mut [Listener<'a>], pub path: &'static [u8], } @@ -118,12 +119,12 @@ where pub struct Listener<'a> { pub name: &'static str, - pub callback: &'a dyn Fn(), + pub callback: &'a mut dyn FnMut(&dyn Any), } #[test] fn what_are_the_sizes() { - dbg!(std::mem::size_of::()); + dbg!(std::mem::size_of::()); dbg!(std::mem::size_of::