From 06ae4fc17833ff9695b0645f940b5ea32eeb4ef0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 24 Feb 2021 03:51:26 -0500 Subject: [PATCH] Feat: wire up rebuild --- .vscode/settings.json | 2 +- packages/core/examples/contextapi.rs | 4 +- packages/core/src/context.rs | 20 ++-- packages/core/src/events.rs | 61 ++++++++----- packages/core/src/lib.rs | 6 +- packages/core/src/scope.rs | 132 +++++++++++++-------------- packages/core/src/virtual_dom.rs | 96 +++++++++++++------ packages/web/examples/hello.rs | 9 +- packages/web/examples/weather.rs | 4 +- packages/web/src/interpreter.rs | 4 +- packages/web/src/lib.rs | 42 ++++----- 11 files changed, 210 insertions(+), 170 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 80f51cff8..2f418701b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } \ No newline at end of file diff --git a/packages/core/examples/contextapi.rs b/packages/core/examples/contextapi.rs index 8d511f326..ae4ec58dd 100644 --- a/packages/core/examples/contextapi.rs +++ b/packages/core/examples/contextapi.rs @@ -1,6 +1,4 @@ - - -use builder::{button}; +use builder::button; use dioxus_core::prelude::*; fn main() {} diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index abb6cc9e4..69c6ac1fb 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,10 +1,11 @@ +use crate::nodes::VNode; use crate::prelude::*; -use crate::{nodes::VNode}; use bumpalo::Bump; use hooks::Hook; +use log::info; use std::{ - any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData, - sync::atomic::AtomicUsize, + any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData, ops::Deref, + rc::Rc, sync::atomic::AtomicUsize, }; /// Components in Dioxus use the "Context" object to interact with their lifecycle. @@ -35,6 +36,8 @@ pub struct Context<'src> { pub(crate) hooks: &'src RefCell>, pub(crate) bump: &'src Bump, + pub(crate) final_nodes: Rc>>>, + // holder for the src lifetime // todo @jon remove this pub _p: std::marker::PhantomData<&'src ()>, @@ -73,10 +76,10 @@ impl<'a> Context<'a> { /// } ///``` pub fn view(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> DomTree { - // pub fn view(self, lazy_nodes: impl for<'b> FnOnce(&'b Bump) -> VNode<'b> + 'a + 'p) -> DomTree { - // pub fn view<'p>(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a + 'p) -> DomTree { - // pub fn view(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> VNode<'a> { - let _g = lazy_nodes(self.bump); + let safe_nodes = lazy_nodes(self.bump); + let unsafe_nodes = unsafe { std::mem::transmute::, VNode<'static>>(safe_nodes) }; + self.final_nodes.deref().borrow_mut().replace(unsafe_nodes); + info!("lazy nodes have been generated"); DomTree {} } @@ -186,9 +189,8 @@ mod context_api { //! a failure of implementation. //! //! - - use std::{ops::Deref}; + use std::ops::Deref; pub struct RemoteState { inner: *const T, diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 6614f106a..34f966397 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -16,34 +16,45 @@ impl EventTrigger { pub fn new() -> Self { todo!() } - - /// Create a new "start" event that boots up the virtual dom if it is paused - pub fn start_event() -> Self { - todo!() - } } pub enum VirtualEvent { - // the event to drain the current lifecycle queue - // Used to initate the dom - StartEvent, - // Real events - ClipboardEvent, - CompositionEvent, - KeyboardEvent, - FocusEvent, - FormEvent, - GenericEvent, - MouseEvent, - PointerEvent, - SelectionEvent, - TouchEvent, - UIEvent, - WheelEvent, - MediaEvent, - ImageEvent, - AnimationEvent, - TransitionEvent, + ClipboardEvent(ClipboardEvent), + CompositionEvent(CompositionEvent), + KeyboardEvent(KeyboardEvent), + FocusEvent(FocusEvent), + FormEvent(FormEvent), + GenericEvent(GenericEvent), + MouseEvent(MouseEvent), + PointerEvent(PointerEvent), + SelectionEvent(SelectionEvent), + TouchEvent(TouchEvent), + UIEvent(UIEvent), + WheelEvent(WheelEvent), + MediaEvent(MediaEvent), + ImageEvent(ImageEvent), + AnimationEvent(AnimationEvent), + TransitionEvent(TransitionEvent), + OtherEvent, } + +// these should reference the underlying event + +pub struct ClipboardEvent {} +pub struct CompositionEvent {} +pub struct KeyboardEvent {} +pub struct FocusEvent {} +pub struct FormEvent {} +pub struct GenericEvent {} +pub struct MouseEvent {} +pub struct PointerEvent {} +pub struct SelectionEvent {} +pub struct TouchEvent {} +pub struct UIEvent {} +pub struct WheelEvent {} +pub struct MediaEvent {} +pub struct ImageEvent {} +pub struct AnimationEvent {} +pub struct TransitionEvent {} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b60736c99..90af768dc 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -88,7 +88,7 @@ pub mod builder { // types used internally that are important pub(crate) mod innerlude { // pub(crate) use crate::component::Properties; - + pub(crate) use crate::context::Context; pub(crate) use crate::error::{Error, Result}; use crate::nodes; @@ -101,9 +101,7 @@ pub(crate) mod innerlude { pub type FC

= for<'scope> fn(Context<'scope>, &'scope P) -> DomTree; - mod fc2 { - - } + mod fc2 {} // pub type FC<'a, P: 'a> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree; // pub type FC

= for<'scope, 'r> fn(Context<'scope>, &'scope P) -> DomTree; // pub type FC

= for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>; diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index ebf57d3f8..8fd737923 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -12,6 +12,7 @@ use std::{ marker::PhantomData, ops::{Deref, DerefMut}, sync::atomic::AtomicUsize, + sync::atomic::Ordering, todo, }; @@ -40,34 +41,23 @@ pub struct Scope { // lying, cheating reference >:( pub props: Box, - // pub props: Box, - // // pub props_type: TypeId, pub caller: *const (), } impl Scope { // create a new scope from a function - pub fn new<'a, P1, P2: 'static>( - // pub fn new<'a, P: Properties, PFree: P + 'a, PLocked: P + 'static>( - f: FC, - props: P1, - parent: Option, - ) -> Self -// where - // PFree: 'a, - // PLocked: 'static, - { - // Capture the props type - // let props_type = TypeId::of::

(); + pub fn new<'a, P1, P2: 'static>(f: FC, props: P1, parent: Option) -> Self { let hook_arena = typed_arena::Arena::new(); let hooks = RefCell::new(Vec::new()); // Capture the caller let caller = f as *const (); - let listeners = Vec::new(); + let listeners: Vec> = vec![Box::new(|| { + log::info!("Base listener called"); + })]; let old_frame = BumpFrame { bump: Bump::new(), @@ -84,16 +74,11 @@ impl Scope { // box the props let props = Box::new(props); - // erase the lifetime - // we'll manage this with dom lifecycle - let props = unsafe { std::mem::transmute::<_, Box>(props) }; - // todo!() Self { hook_arena, hooks, - // props_type, caller, frames, listeners, @@ -102,13 +87,6 @@ impl Scope { } } - /// Update this component's props with a new set of props, remotely - /// - /// - pub(crate) fn update_props<'a, P>(&self, _new_props: P) -> crate::error::Result<()> { - Ok(()) - } - /// Create a new context and run the component with references from the Virtual Dom /// This function downcasts the function pointer based on the stored props_type /// @@ -120,67 +98,67 @@ impl Scope { frame }; + let node_slot = std::rc::Rc::new(RefCell::new(None)); let ctx: Context<'bump> = Context { arena: &self.hook_arena, hooks: &self.hooks, bump: &frame.bump, idx: 0.into(), _p: PhantomData {}, + final_nodes: node_slot.clone(), }; unsafe { + /* + SAFETY ALERT + + This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html + We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called, + we transmute the function back using the props as reference. + + we could do a better check to make sure that the TypeID is correct before casting + -- + This is safe because we check that the generic type matches before casting. + */ // we use plocked to be able to remove the borrowed lifetime // these lifetimes could be very broken, so we need to dynamically manage them let caller = std::mem::transmute::<*const (), FC>(self.caller); let props = self.props.downcast_ref::().unwrap(); + + // Note that the actual modification of the vnode head element occurs during this call let _nodes: DomTree = caller(ctx, props); - todo!("absorb domtree into self") - // let nodes: VNode<'bump> = caller(ctx, props); - // let unsafe_node = std::mem::transmute::, VNode<'static>>(nodes); - // frame.head_node = unsafe_node; + /* + SAFETY ALERT + + DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS. + KEEPING THIS STATIC REFERENCE CAN LEAD TO UB. + + Some things to note: + - The VNode itself is bound to the lifetime, but it itself is owned by scope. + - The VNode has a private API and can only be used from accessors. + - Public API cannot drop or destructure VNode + */ + // the nodes we care about have been unsafely extended to a static lifetime in context + frame.head_node = node_slot + .deref() + .borrow_mut() + .take() + .expect("Viewing did not happen"); } - /* - SAFETY ALERT - - This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html - We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called, - we transmute the function back using the props as reference. - - we could do a better check to make sure that the TypeID is correct before casting - -- - This is safe because we check that the generic type matches before casting. - */ - - /* - SAFETY ALERT - - DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS. - KEEPING THIS STATIC REFERENCE CAN LEAD TO UB. - - Some things to note: - - The VNode itself is bound to the lifetime, but it itself is owned by scope. - - The VNode has a private API and can only be used from accessors. - - Public API cannot drop or destructure VNode - */ } /// Accessor to get the root node and its children (safely)\ /// Scope is self-referntial, so we are forced to use the 'static lifetime to cheat - pub fn current_root_node<'bump>(&'bump self) -> &'bump VNode<'bump> { + pub fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { self.frames.current_head_node() } - pub fn prev_root_node<'bump>(&'bump self) -> &'bump VNode<'bump> { - todo!() + pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { + self.frames.prev_head_node() } } -pub struct BumpFrame { - pub bump: Bump, - pub head_node: VNode<'static>, -} - // todo, do better with the active frame stuff // somehow build this vnode with a lifetime tied to self // This root node has "static" lifetime, but it's really not static. @@ -192,6 +170,11 @@ pub struct ActiveFrame { pub frames: [BumpFrame; 2], } +pub struct BumpFrame { + pub bump: Bump, + pub head_node: VNode<'static>, +} + impl ActiveFrame { fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { Self { @@ -201,8 +184,23 @@ impl ActiveFrame { } fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { - let cur_idx = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed) % 1; - let raw_node = &self.frames[cur_idx]; + let raw_node = match self.idx.borrow().load(Ordering::Relaxed) & 1 != 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + unsafe { + let unsafe_head = &raw_node.head_node; + let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); + safe_node + } + } + + fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { + let raw_node = match self.idx.borrow().load(Ordering::Relaxed) & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + unsafe { let unsafe_head = &raw_node.head_node; let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); @@ -211,8 +209,8 @@ impl ActiveFrame { } fn next(&mut self) -> &mut BumpFrame { - self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let cur = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed); + self.idx.fetch_add(1, Ordering::Relaxed); + let cur = self.idx.borrow().load(Ordering::Relaxed); match cur % 1 { 1 => &mut self.frames[1], 0 => &mut self.frames[0], @@ -276,7 +274,7 @@ mod tests { }) } - fn child_example(ctx: Context, props: &ExampleProps) -> DomTree { + fn child_example<'b>(ctx: Context<'b>, props: &'b ExampleProps) -> DomTree { ctx.view(move |b| { div(b) .child(text(props.name)) diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1b783d65a..81adf080c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -78,6 +78,34 @@ impl VirtualDom { } } + //// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom + pub fn rebuild(&mut self) -> Result> { + // Reset and then build a new diff machine + // The previous edit list cannot be around while &mut is held + // Make sure variance doesnt break this + self.diff_bump.reset(); + let mut diff_machine = DiffMachine::new(&self.diff_bump); + + // this is still a WIP + // we'll need to re-fecth all the scopes that were changed and build the diff machine + // fetch the component again + let component = self + .components + .get_mut(self.base_scope) + .expect("Root should always exist"); + + component.run::<()>(); + + let old = component.old_frame(); + let new = component.new_frame(); + dbg!(old); + dbg!(new); + + diff_machine.diff_node(old, new); + + Ok(diff_machine.consume()) + } + /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. /// /// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual @@ -102,17 +130,16 @@ impl VirtualDom { /// } /// /// ``` - pub fn progress_with_event(&mut self, evt: EventTrigger) -> Result> { + pub fn progress_with_event(&mut self, event: EventTrigger) -> Result> { let EventTrigger { component_id, listener_id, event: _, - } = evt; + } = event; let component = self .components .get(component_id) - // todo: update this with a dedicated error type so implementors know what went wrong .expect("Component should exist if an event was triggered"); let listener = component @@ -121,47 +148,37 @@ impl VirtualDom { .expect("Listener should exist if it was triggered") .as_ref(); - // Run the callback - // This should cause internal state to progress, dumping events into the event queue - // todo: integrate this with a tracing mechanism exposed to a dev tool + // Run the callback with the user event listener(); - // Run through our events, tagging which Indexes are receiving updates - // Prop updates take prescedence over subscription updates - // Run all prop updates *first* as they will cascade into children. - // *then* run the non-prop updates that were not already covered by props - + // Mark dirty components. Descend from the highest node until all dirty nodes are updated. let mut affected_components = Vec::new(); - // It's essentially draining the vec, but with some dancing to release the RefMut - // We also want to be able to push events into the queue from processing the event - while let Some(event) = { - let new_evt = self.event_queue.as_ref().borrow_mut().pop_front(); - new_evt - } { + while let Some(event) = self.pop_event() { if let Some(component_idx) = event.index() { affected_components.push(component_idx); } self.process_lifecycle(event)?; } - // Reset and then build a new diff machine - // The previous edit list cannot be around while &mut is held - // Make sure variance doesnt break this - self.diff_bump.reset(); - let diff_machine = DiffMachine::new(&self.diff_bump); - - Ok(diff_machine.consume()) + todo!() } /// Using mutable access to the Virtual Dom, progress a given lifecycle event fn process_lifecycle(&mut self, LifecycleEvent { event_type }: LifecycleEvent) -> Result<()> { match event_type { // Component needs to be mounted to the virtual dom - LifecycleType::Mount { to: _, under: _, props: _ } => {} + LifecycleType::Mount { + to: _, + under: _, + props: _, + } => {} // The parent for this component generated new props and the component needs update - LifecycleType::PropsChanged { props: _, component: _ } => {} + LifecycleType::PropsChanged { + props: _, + component: _, + } => {} // Component was messaged via the internal subscription service LifecycleType::Callback { component: _ } => {} @@ -170,6 +187,11 @@ impl VirtualDom { Ok(()) } + /// Pop the top event of the internal lifecycle event queu + pub fn pop_event(&self) -> Option { + self.event_queue.as_ref().borrow_mut().pop_front() + } + /// With access to the virtual dom, schedule an update to the Root component's props. /// This generates the appropriate Lifecycle even. It's up to the renderer to actually feed this lifecycle event /// back into the event system to get an edit list. @@ -215,10 +237,30 @@ pub enum LifecycleType { impl LifecycleEvent { fn index(&self) -> Option { match &self.event_type { - LifecycleType::Mount { to: _, under: _, props: _ } => None, + LifecycleType::Mount { + to: _, + under: _, + props: _, + } => None, LifecycleType::PropsChanged { component, .. } | LifecycleType::Callback { component } => Some(component.clone()), } } } + +mod tests { + use super::*; + + #[test] + fn start_dom() { + let mut dom = VirtualDom::new(|ctx, props| { + ctx.view(|bump| { + use crate::builder::*; + div(bump).child(text("hello, world")).finish() + }) + }); + let edits = dom.rebuild().unwrap(); + println!("{:#?}", edits); + } +} diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index eec9c1fe8..acc9f5ff4 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -4,15 +4,10 @@ use dioxus_web::WebsysRenderer; fn main() { // todo: set this up so the websys render can spawn itself rather than having to wrap it // almost like bundling an executor with the wasm version - wasm_bindgen_futures::spawn_local(async { - WebsysRenderer::new(Example) - .run() - .await - .expect("Dioxus Failed! This should *not* happen!") - }); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } -static Example: FC<()> = |ctx, props| { +static Example: FC<()> = |ctx, _props| { ctx.view(html! {

"Hello world!" diff --git a/packages/web/examples/weather.rs b/packages/web/examples/weather.rs index c54fbb86c..36c152370 100644 --- a/packages/web/examples/weather.rs +++ b/packages/web/examples/weather.rs @@ -9,7 +9,7 @@ fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); console_error_panic_hook::set_once(); - WebsysRenderer::new(|ctx, _| { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|ctx, _| { ctx.view(html! {
@@ -43,5 +43,5 @@ fn main() {
}) - }); + })); } diff --git a/packages/web/src/interpreter.rs b/packages/web/src/interpreter.rs index 747c26f5b..60769850d 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/interpreter.rs @@ -1,6 +1,6 @@ use dioxus_core::changelist::Edit; use fxhash::FxHashMap; -use log::{debug, info, log}; +use log::{debug}; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{window, Document, Element, Event, Node}; @@ -95,7 +95,7 @@ impl PatchMachine { self.templates.get(&id) } - pub fn init_events_trampoline(&mut self, mut trampoline: ()) { + pub fn init_events_trampoline(&mut self, _trampoline: ()) { todo!("Event trampoline not a thing anymore") // pub fn init_events_trampoline(&mut self, mut trampoline: EventsTrampoline) { // self.callback = Some(Closure::wrap(Box::new(move |event: &web_sys::Event| { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 2782ac691..bde261993 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -5,20 +5,14 @@ use fxhash::FxHashMap; use web_sys::{window, Document, Element, Event, Node}; -use dioxus::prelude::VElement; - pub use dioxus_core as dioxus; use dioxus_core::{ events::EventTrigger, - prelude::{bumpalo::Bump, html, DiffMachine, VNode, VirtualDom, FC}, + prelude::{VirtualDom, FC}, }; -use futures::{ - channel::mpsc::{self, Sender}, - future, SinkExt, StreamExt, -}; -use mpsc::UnboundedSender; +use futures::{channel::mpsc, SinkExt, StreamExt}; + pub mod interpreter; -use interpreter::PatchMachine; /// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM. /// Under the hood, we leverage WebSys and interact directly with the DOM @@ -27,7 +21,7 @@ pub struct WebsysRenderer { internal_dom: VirtualDom, // this should be a component index - event_map: FxHashMap<(u32, u32), u32>, + _event_map: FxHashMap<(u32, u32), u32>, } impl WebsysRenderer { @@ -57,34 +51,36 @@ impl WebsysRenderer { pub fn from_vdom(dom: VirtualDom) -> Self { Self { internal_dom: dom, - event_map: FxHashMap::default(), + _event_map: FxHashMap::default(), } } pub async fn run(&mut self) -> dioxus_core::error::Result<()> { - let (mut sender, mut receiver) = mpsc::unbounded::(); - - sender - .send(EventTrigger::start_event()) - .await - .expect("Should not fail"); + let (sender, mut receiver) = mpsc::unbounded::(); let body = prepare_websys_dom(); - let mut patch_machine = PatchMachine::new(body.clone()); + let mut patch_machine = interpreter::PatchMachine::new(body.clone()); let root_node = body.first_child().unwrap(); patch_machine.stack.push(root_node); + self.internal_dom + .rebuild()? + .into_iter() + .for_each(|edit| patch_machine.handle_edit(&edit)); + // Event loop waits for the receiver to finish up // TODO! Connect the sender to the virtual dom's suspense system // Suspense is basically an external event that can force renders to specific nodes while let Some(event) = receiver.next().await { - let diffs = self.internal_dom.progress_with_event(event)?; - for edit in &diffs { - patch_machine.handle_edit(edit); - } + self.internal_dom + .progress_with_event(event)? + .into_iter() + .for_each(|edit| { + patch_machine.handle_edit(&edit); + }); } - Ok(()) // should actually never return from this, should be an error + Ok(()) // should actually never return from this, should be an error, rustc just cant see it } }