From c28697e1fe3136d1835f2b663715f34aab9f4b17 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 May 2021 02:06:02 -0400 Subject: [PATCH] Feat: some code health --- .vscode/spellright.dict | 1 + Cargo.toml | 4 +- README.md | 25 +- notes/CHANGELOG.md | 20 +- packages/core-macro/src/htm.rs | 2 +- packages/core-macro/src/rsxt.rs | 2 +- packages/core/Cargo.toml | 9 +- packages/core/examples/alternative.rs | 2 +- packages/core/examples/nested.rs | 2 +- packages/core/src/component.rs | 53 --- packages/core/src/context.rs | 373 ++++------------- packages/core/src/events.rs | 7 +- packages/core/src/lib.rs | 4 +- packages/core/src/nodebuilder.rs | 29 +- packages/core/src/virtual_dom.rs | 567 +++++++++++++++++++------- packages/web/examples/rsxt.rs | 23 +- 16 files changed, 570 insertions(+), 553 deletions(-) diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 9ee8d693..06cd932a 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -29,3 +29,4 @@ datafetching partialeq rsx Ctx +fmt diff --git a/Cargo.toml b/Cargo.toml index 3e92bd80..003641cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ members = [ "packages/core-macro", "packages/core", "packages/web", - "packages/webview", "packages/dioxus", -] + +] # "packages/webview", # "packages/docsite", # "packages/ssr", diff --git a/README.md b/README.md index 5167c846..e6e2bbc7 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,11 @@

- - Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust. ```rust -static Example: FC<()> = |ctx, props| { +fn Example(ctx: Context, props: &()) -> DomTree { let selection = use_state(&ctx, || "...?"); ctx.render(rsx! { @@ -80,24 +78,3 @@ In other frameworks, the DOM will be updated after events from the page are sent In Dioxus, the user's bundle will link individual components on the page to the Liveview server. This ensures local events propogate through the page virtual dom if the server is not needed, keeping interactive latency low. This ensures the server load stays low, enabling a single server to handle tens of thousands of simultaneous clients. - - - - - - - - - diff --git a/notes/CHANGELOG.md b/notes/CHANGELOG.md index b2dec697..97567d70 100644 --- a/notes/CHANGELOG.md +++ b/notes/CHANGELOG.md @@ -2,9 +2,7 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings support for: - Web via WASM - Desktop via webview integration -- Server-rendering with custom Display Impl -- Liveview (experimental) -- Mobile (experimental) +- Server-rendering with custom ToString implementation - State management - Build CLI ---- @@ -79,12 +77,14 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [x] keys on components - [x] Allow paths for components - [x] todo mvc -- [ ] Make events lazy (use traits + Box) -- [ ] Attributes on elements should implement format_args -- [ ] Beef up the dioxus CLI tool -- [ ] Tweak macro parsing for better errors +- [ ] Make events lazy (use traits + Box) - not sure what this means anymore +- [ ] Attributes on elements should implement format_args instead of string fmt +- [ ] Beef up the dioxus CLI tool to report build progress +- [x] Tweak macro parsing for better errors +- [ ] Extract arena logic out for better safety guarantees +- [ ] Extract BumpFrame logic out for better safety guarantees - [ ] make SSR follow HTML spec -- [ ] dirty tagging, compression +- [x] dirty tagging, compression - [ ] fix keys on elements - [ ] MIRI tests - [ ] code health @@ -92,11 +92,11 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [ ] double check event targets and stuff - [ ] Documentation overhaul - [ ] Website -= [ ] controlled components +- [ ] controlled components lower priority features - [ ] fragments - [ ] node refs (postpone for future release?) - [ ] styling built-in (future release?) - [ ] key handler? -- [ ] FC macro +- [ ] FC macro? diff --git a/packages/core-macro/src/htm.rs b/packages/core-macro/src/htm.rs index 315d7712..a61f2494 100644 --- a/packages/core-macro/src/htm.rs +++ b/packages/core-macro/src/htm.rs @@ -59,7 +59,7 @@ impl ToTokens for HtmlRender { // create a lazy tree that accepts a bump allocator let final_tokens = quote! { dioxus::prelude::LazyNodes::new(move |ctx| { - let bump = ctx.bump; + let bump = &ctx.bump(); #new_toks }) diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index 8dbdd36c..12df6ce8 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -51,7 +51,7 @@ impl ToTokens for RsxRender { let inner = &self.root; let output = quote! { dioxus::prelude::LazyNodes::new(move |ctx|{ - let bump = ctx.bump; + let bump = &ctx.bump(); #inner }) }; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 28dc56f2..3a41b948 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -20,7 +20,7 @@ generational-arena = { version = "0.2.8", features = ["serde"] } bumpalo = { version = "3.6.0", features = ["collections", "boxed"] } # custom error type -thiserror = "1.0.23" +thiserror = "1" # faster hashmaps fxhash = "0.2.1" @@ -29,9 +29,10 @@ fxhash = "0.2.1" longest-increasing-subsequence = "0.1.0" # internall used -log = "0.4.14" +log = "0.4" -serde = { version = "1.0.123", features = ["derive"], optional = true } +# Serialize the Edits for use in Webview/Liveview instances +serde = { version = "1", features = ["derive"], optional = true } [features] -default = ["serde"] +default = [] diff --git a/packages/core/examples/alternative.rs b/packages/core/examples/alternative.rs index 25ea11f3..0ca5cc2d 100644 --- a/packages/core/examples/alternative.rs +++ b/packages/core/examples/alternative.rs @@ -4,7 +4,7 @@ use dioxus_core::prelude::*; static Example: FC<()> = |ctx, props| { ctx.render(dioxus_core::prelude::LazyNodes::new(move |ctx| { - let bump = ctx.bump; + let bump = ctx.bump(); dioxus::builder::ElementBuilder::new(ctx, "h1") .children([{ use bumpalo::core_alloc::fmt::Write; diff --git a/packages/core/examples/nested.rs b/packages/core/examples/nested.rs index dc11cf36..00d41cca 100644 --- a/packages/core/examples/nested.rs +++ b/packages/core/examples/nested.rs @@ -17,7 +17,7 @@ static Header: FC<()> = |ctx, props| { .child(VNode::Component(VComponent::new( Bottom, // - c.bump.alloc(()), + c.bump().alloc(()), None, ))) .finish() diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index e1e9b400..950ba45d 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -7,61 +7,8 @@ use crate::innerlude::FC; -use self::sized_any::SizedAny; - pub type ScopeIdx = generational_arena::Index; -struct ComparableComp<'s> { - fc_raw: *const (), - f: &'s dyn SizedAny, -} - -impl<'s> ComparableComp<'s> { - fn compare_to(&self, other: &ComparableComp) -> bool { - if self.fc_raw == other.fc_raw { - let real_other = unsafe { &*(other.f as *const _ as *const P) }; - true - } else { - false - } - } -} - -struct TestProps {} - -fn test() {} - -mod sized_any { - use std::any::TypeId; - - // don't allow other implementations of `SizedAny`; `SizedAny` must only be - // implemented for sized types. - mod seal { - // it must be a `pub trait`, but not be reachable - hide it in - // private mod. - pub trait Seal {} - } - - pub trait SizedAny: seal::Seal {} - - impl seal::Seal for T {} - impl SizedAny for T {} - - // `SizedAny + ?Sized` means it can be a trait object, but `SizedAny` was - // implemented for the underlying sized type. - pub fn downcast_ref(v: &From) -> Option<&To> - where - From: SizedAny + ?Sized + 'static, - To: 'static, - { - // if TypeId::of::() == < From as SizedAny>::get_type_id(v) { - Some(unsafe { &*(v as *const From as *const To) }) - // } else { - // None - // } - } -} - pub trait Properties: PartialEq { type Builder; fn builder() -> Self::Builder; diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index efe46481..6fdf0b76 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,203 +1,8 @@ use crate::{innerlude::*, nodebuilder::IntoDomTree}; use crate::{nodebuilder::LazyNodes, nodes::VNode}; use bumpalo::Bump; -use hooks::Hook; use std::{cell::RefCell, future::Future, ops::Deref, pin::Pin, rc::Rc, sync::atomic::AtomicUsize}; -/// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// This lets components schedule updates, integrate hooks, and expose their context via the context api. -/// -/// Properties passed down from the parent component are also directly accessible via the exposed "props" field. -/// -/// ```ignore -/// #[derive(Properties)] -/// struct Props { -/// name: String -/// -/// } -/// -/// fn example(ctx: Context, props: &Props -> VNode { -/// html! { -///
"Hello, {ctx.props.name}"
-/// } -/// } -/// ``` -// todo: force lifetime of source into T as a valid lifetime too -// it's definitely possible, just needs some more messing around -pub struct Context<'src> { - pub idx: RefCell, - - // pub scope: ScopeIdx, - pub scope: &'src Scope, - // // Borrowed from scope - // pub(crate) hooks: &'src RefCell>, - // pub(crate) bump: &'src Bump, - // pub listeners: &'src RefCell>, - - // holder for the src lifetime - // todo @jon remove this - pub _p: std::marker::PhantomData<&'src ()>, -} - -impl<'a> Context<'a> { - /// Access the children elements passed into the component - pub fn children(&self) -> Vec { - todo!("Children API not yet implemented for component Context") - } - - pub fn callback(&self, _f: impl Fn(()) + 'a) {} - - // call this closure after the component has been committed to the editlist - // this provides the founation of "use_effect" - fn post_update() {} - - /// Create a subscription that schedules a future render for the reference component - pub fn schedule_update(&self) -> impl Fn() -> () { - || {} - } - - /// Create a suspended component from a future. - /// - /// When the future completes, the component will be renderered - pub fn suspend FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>( - &self, - _fut: impl Future>, - ) -> VNode<'a> { - todo!() - } -} - -// NodeCtx is used to build VNodes in the component's memory space. -// This struct adds metadata to the final DomTree about listeners, attributes, and children -#[derive(Debug, Clone)] -pub struct NodeCtx<'a> { - pub bump: &'a Bump, - pub listeners: &'a RefCell>, - pub idx: RefCell, - pub scope: ScopeIdx, -} - -impl<'a> Context<'a> { - /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. - /// - /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. - /// - /// ## Example - /// - /// ```ignore - /// fn Component(ctx: Context) -> VNode { - /// // Lazy assemble the VNode tree - /// let lazy_tree = html! {
"Hello World"
}; - /// - /// // Actually build the tree and allocate it - /// ctx.render(lazy_tree) - /// } - ///``` - pub fn render FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>( - &self, - lazy_nodes: LazyNodes<'a, F>, - ) -> DomTree { - // let idx = self.idx.borrow(); - let ctx = NodeCtx { - bump: &self.scope.cur_frame().bump, - scope: self.scope.myidx, - - // hmmmmmmmm not sure if this is right - idx: 0.into(), - listeners: &self.scope.listeners, - }; - - let safe_nodes = lazy_nodes.into_vnode(&ctx); - let root: VNode<'static> = unsafe { std::mem::transmute(safe_nodes) }; - DomTree { root } - } -} - -/// This module provides internal state management functionality for Dioxus components -pub mod hooks { - use std::any::Any; - - use super::*; - - #[derive(Debug)] - pub struct Hook(pub Pin>); - - impl Hook { - pub fn new(state: T) -> Self { - Self(Box::pin(state)) - } - } - - impl<'a> Context<'a> { - /// TODO: @jon, rework this so we dont have to use unsafe to make hooks and then return them - /// use_hook provides a way to store data between renders for functional components. - /// todo @jon: ensure the hook arena is stable with pin or is stable by default - pub fn use_hook<'scope, InternalHookState: 'static, Output: 'a>( - &'scope self, - // The closure that builds the hook state - initializer: impl FnOnce() -> InternalHookState, - // The closure that takes the hookstate and returns some value - runner: impl FnOnce(&'a mut InternalHookState) -> Output, - // The closure that cleans up whatever mess is left when the component gets torn down - // TODO: add this to the "clean up" group for when the component is dropped - _cleanup: impl FnOnce(InternalHookState), - ) -> Output { - let idx = *self.idx.borrow(); - - // Mutate hook list if necessary - let mut hooks = self.scope.hooks.borrow_mut(); - - // Initialize the hook by allocating it in the typed arena. - // We get a reference from the arena which is owned by the component scope - // This is valid because "Context" is only valid while the scope is borrowed - if idx >= hooks.len() { - let new_state = initializer(); - hooks.push(Hook::new(new_state)); - } - - *self.idx.borrow_mut() += 1; - - let stable_ref = hooks.get_mut(idx).unwrap().0.as_mut(); - let v = unsafe { Pin::get_unchecked_mut(stable_ref) }; - let internal_state = v - .downcast_mut::() - .expect("couldn't find the hook state"); - - // we extend the lifetime from borrowed in this scope to borrowed from self. - // This is okay because the hook is pinned - runner(unsafe { &mut *(internal_state as *mut _) }) - - /* - ** UNSAFETY ALERT ** - Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay. - - However, typed-arena gives a mutable reference to the stored data which is stable for any inserts - into the arena. During the first call of the function, we need to add the mutable reference given to us by - the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated - when the component itself is deallocated. - - This is okay because: - - The lifetime of the component arena is tied to the lifetime of these raw hooks - - Usage of the raw hooks is tied behind the Vec refcell - - Output is static, meaning it can't take a reference to the data - - We don't expose the raw hook pointer outside of the scope of use_hook - - The reference is tied to context, meaning it can only be used while ctx is around to free it - */ - // let raw_hook: &'scope mut _ = unsafe { &mut *raw_hook }; - // let p = raw_hook.0.downcast_mut::(); - // let r = p.unwrap(); - // let v = unsafe { Pin::get_unchecked_mut(raw_hook) }; - // // let carreied_ref: &'scope mut dyn Any = unsafe { &mut *v }; - // let internal_state = v.downcast_mut::().unwrap(); - - // let real_internal = unsafe { internal_state as *mut _ }; - - // runner(unsafe { &mut *real_internal }) - // runner(internal_state) - } - } -} - /// Context API /// /// The context API provides a mechanism for components to borrow state from other components higher in the tree. @@ -212,111 +17,107 @@ pub mod hooks { /// Instead of placing the onus on the receiver of the data to use it properly, we wrap the source object in a /// "shield" where gaining &mut access can only be done if no active StateGuards are open. This would fail and indicate /// a failure of implementation. -pub mod context_api { - use std::ops::Deref; +pub struct RemoteState { + inner: *const T, +} +impl Copy for RemoteState {} - pub struct RemoteState { - inner: *const T, +impl Clone for RemoteState { + fn clone(&self) -> Self { + Self { inner: self.inner } } - impl Copy for RemoteState {} +} - impl Clone for RemoteState { - fn clone(&self) -> Self { - Self { inner: self.inner } - } - } - - static DEREF_ERR_MSG: &'static str = r#""" +static DEREF_ERR_MSG: &'static str = r#""" [ERROR] This state management implementation is faulty. Report an issue on whatever implementation is using this. Context should *never* be dangling!. If a Context is torn down, so should anything that references it. """#; - impl Deref for RemoteState { - type Target = T; +impl Deref for RemoteState { + type Target = T; - fn deref(&self) -> &Self::Target { - // todo! - // Try to borrow the underlying context manager, register this borrow with the manager as a "weak" subscriber. - // This will prevent the panic and ensure the pointer still exists. - // For now, just get an immutable reference to the underlying context data. - // - // It's important to note that ContextGuard is not a public API, and can only be made from UseContext. - // This guard should only be used in components, and never stored in hooks - unsafe { - match self.inner.as_ref() { - Some(ptr) => ptr, - None => panic!(DEREF_ERR_MSG), - } + fn deref(&self) -> &Self::Target { + // todo! + // Try to borrow the underlying context manager, register this borrow with the manager as a "weak" subscriber. + // This will prevent the panic and ensure the pointer still exists. + // For now, just get an immutable reference to the underlying context data. + // + // It's important to note that ContextGuard is not a public API, and can only be made from UseContext. + // This guard should only be used in components, and never stored in hooks + unsafe { + match self.inner.as_ref() { + Some(ptr) => ptr, + None => panic!(DEREF_ERR_MSG), } } } +} - impl<'a> super::Context<'a> { - // impl<'a, P> super::Context<'a, P> { - pub fn use_context(&'a self, _narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState { - todo!() - } - - pub fn create_context(&self, creator: impl FnOnce() -> T) {} +impl<'a> crate::virtual_dom::Context<'a> { + // impl<'a, P> super::Context<'a, P> { + pub fn use_context(&'a self, _narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState { + todo!() } - /// # SAFETY ALERT - /// - /// The underlying context mechanism relies on mutating &mut T while &T is held by components in the tree. - /// By definition, this is UB. Therefore, implementing use_context should be done with upmost care to invalidate and - /// prevent any code where &T is still being held after &mut T has been taken and T has been mutated. - /// - /// While mutating &mut T while &T is captured by listeners, we can do any of: - /// 1) Prevent those listeners from being called and avoid "producing" UB values - /// 2) Delete instances of closures where &T is captured before &mut T is taken - /// 3) Make clones of T to preserve the original &T. - /// 4) Disable any &T remotely (like RwLock, RefCell, etc) - /// - /// To guarantee safe usage of state management solutions, we provide Dioxus-Reducer and Dioxus-Dataflow built on the - /// SafeContext API. This should provide as an example of how to implement context safely for 3rd party state management. - /// - /// It's important to recognize that while safety is a top concern for Dioxus, ergonomics do take prescendence. - /// Contrasting with the JS ecosystem, Rust is faster, but actually "less safe". JS is, by default, a "safe" language. - /// However, it does not protect you against data races: the primary concern for 3rd party implementers of Context. - /// - /// We guarantee that any &T will remain consistent throughout the life of the Virtual Dom and that - /// &T is owned by components owned by the VirtualDom. Therefore, it is impossible for &T to: - /// - be dangling or unaligned - /// - produce an invalid value - /// - produce uninitialized memory - /// - /// The only UB that is left to the implementer to prevent are Data Races. - /// - /// Here's a strategy that is UB: - /// 1. &T is handed out via use_context - /// 2. an event is reduced against the state - /// 3. An &mut T is taken - /// 4. &mut T is mutated. - /// - /// Now, any closures that caputed &T are subject to a data race where they might have skipped checks and UB - /// *will* affect the program. - /// - /// Here's a strategy that's not UB (implemented by SafeContext): - /// 1. ContextGuard is handed out via use_context. - /// 2. An event is reduced against the state. - /// 3. The state is cloned. - /// 4. All subfield selectors are evaluated and then diffed with the original. - /// 5. Fields that have changed have their ContextGuard poisoned, revoking their ability to take &T.a. - /// 6. The affected fields of Context are mutated. - /// 7. Scopes with poisoned guards are regenerated so they can take &T.a again, calling their lifecycle. - /// - /// In essence, we've built a "partial borrowing" mechanism for Context objects. - /// - /// ================= - /// nb - /// ================= - /// If you want to build a state management API directly and deal with all the unsafe and UB, we provide - /// `use_context_unchecked` with all the stability with *no* guarantess of Data Race protection. You're on - /// your own to not affect user applications. - /// - /// - Dioxus reducer is built on the safe API and provides a useful but slightly limited API. - /// - Dioxus Dataflow is built on the unsafe API and provides an even snazzier API than Dioxus Reducer. - fn blah() {} + pub fn create_context(&self, creator: impl FnOnce() -> T) {} } + +/// # SAFETY ALERT +/// +/// The underlying context mechanism relies on mutating &mut T while &T is held by components in the tree. +/// By definition, this is UB. Therefore, implementing use_context should be done with upmost care to invalidate and +/// prevent any code where &T is still being held after &mut T has been taken and T has been mutated. +/// +/// While mutating &mut T while &T is captured by listeners, we can do any of: +/// 1) Prevent those listeners from being called and avoid "producing" UB values +/// 2) Delete instances of closures where &T is captured before &mut T is taken +/// 3) Make clones of T to preserve the original &T. +/// 4) Disable any &T remotely (like RwLock, RefCell, etc) +/// +/// To guarantee safe usage of state management solutions, we provide Dioxus-Reducer and Dioxus-Dataflow built on the +/// SafeContext API. This should provide as an example of how to implement context safely for 3rd party state management. +/// +/// It's important to recognize that while safety is a top concern for Dioxus, ergonomics do take prescendence. +/// Contrasting with the JS ecosystem, Rust is faster, but actually "less safe". JS is, by default, a "safe" language. +/// However, it does not protect you against data races: the primary concern for 3rd party implementers of Context. +/// +/// We guarantee that any &T will remain consistent throughout the life of the Virtual Dom and that +/// &T is owned by components owned by the VirtualDom. Therefore, it is impossible for &T to: +/// - be dangling or unaligned +/// - produce an invalid value +/// - produce uninitialized memory +/// +/// The only UB that is left to the implementer to prevent are Data Races. +/// +/// Here's a strategy that is UB: +/// 1. &T is handed out via use_context +/// 2. an event is reduced against the state +/// 3. An &mut T is taken +/// 4. &mut T is mutated. +/// +/// Now, any closures that caputed &T are subject to a data race where they might have skipped checks and UB +/// *will* affect the program. +/// +/// Here's a strategy that's not UB (implemented by SafeContext): +/// 1. ContextGuard is handed out via use_context. +/// 2. An event is reduced against the state. +/// 3. The state is cloned. +/// 4. All subfield selectors are evaluated and then diffed with the original. +/// 5. Fields that have changed have their ContextGuard poisoned, revoking their ability to take &T.a. +/// 6. The affected fields of Context are mutated. +/// 7. Scopes with poisoned guards are regenerated so they can take &T.a again, calling their lifecycle. +/// +/// In essence, we've built a "partial borrowing" mechanism for Context objects. +/// +/// ================= +/// nb +/// ================= +/// If you want to build a state management API directly and deal with all the unsafe and UB, we provide +/// `use_context_unchecked` with all the stability with *no* guarantess of Data Race protection. You're on +/// your own to not affect user applications. +/// +/// - Dioxus reducer is built on the safe API and provides a useful but slightly limited API. +/// - Dioxus Dataflow is built on the unsafe API and provides an even snazzier API than Dioxus Reducer. +fn blah() {} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 761ad629..1a42f278 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -58,8 +58,8 @@ pub mod on { use crate::{ builder::ElementBuilder, - context::NodeCtx, innerlude::{Attribute, Listener, VNode}, + virtual_dom::NodeCtx, }; use super::VirtualEvent; @@ -77,11 +77,12 @@ pub mod on { c: &'_ NodeCtx<'a>, callback: impl Fn($eventdata) + 'a, ) -> Listener<'a> { + let bump = &c.bump(); Listener { event: stringify!($name), id: *c.idx.borrow(), - scope: c.scope, - callback: c.bump.alloc(move |evt: VirtualEvent| match evt { + scope: c.scope_ref.myidx, + callback: bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$eventdata(event) => callback(event), _ => { unreachable!("Downcasted VirtualEvent to wrong event type - this is a bug!") diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 85479ea9..ddb9652d 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -112,13 +112,13 @@ pub(crate) mod innerlude { /// Essential when working with the html! macro pub mod prelude { pub use crate::component::{fc_to_builder, Properties}; - pub use crate::context::Context; use crate::nodes; + pub use crate::virtual_dom::Context; pub use nodes::*; pub use crate::nodebuilder::LazyNodes; - pub use crate::context::NodeCtx; + pub use crate::virtual_dom::NodeCtx; // pub use nodes::iterables::IterableNodes; /// This type alias is an internal way of abstracting over the static functions that represent components. pub use crate::innerlude::FC; diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 8ed6b5f4..a105da2e 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -3,11 +3,11 @@ use std::{any::Any, borrow::BorrowMut, intrinsics::transmute, u128}; use crate::{ - context::NodeCtx, events::VirtualEvent, innerlude::{DomTree, Properties, VComponent, FC}, nodes::{Attribute, Listener, NodeKey, VNode}, prelude::VElement, + virtual_dom::NodeCtx, }; /// A virtual DOM element builder. @@ -64,7 +64,7 @@ impl<'a, 'b> /// # fn flip_coin() -> bool { true } /// ``` pub fn new(ctx: &'b NodeCtx<'a>, tag_name: &'static str) -> Self { - let bump = ctx.bump; + let bump = ctx.bump(); ElementBuilder { ctx, key: NodeKey::NONE, @@ -286,17 +286,19 @@ where /// ``` #[inline] pub fn finish(self) -> VNode<'a> { - let children: &'a Children = self.ctx.bump.alloc(self.children); + let bump = self.ctx.bump(); + + let children: &'a Children = bump.alloc(self.children); let children: &'a [VNode<'a>] = children.as_ref(); - let listeners: &'a Listeners = self.ctx.bump.alloc(self.listeners); + let listeners: &'a Listeners = bump.alloc(self.listeners); let listeners: &'a [Listener<'a>] = listeners.as_ref(); - let attributes: &'a Attributes = self.ctx.bump.alloc(self.attributes); + let attributes: &'a Attributes = bump.alloc(self.attributes); let attributes: &'a [Attribute<'a>] = attributes.as_ref(); VNode::element( - self.ctx.bump, + bump, self.key, self.tag_name, listeners, @@ -334,11 +336,13 @@ where /// .finish(); /// ``` pub fn on(self, event: &'static str, callback: impl Fn(VirtualEvent) + 'a) -> Self { + let bump = &self.ctx.bump(); + let listener = Listener { event, - callback: self.ctx.bump.alloc(callback), + callback: bump.alloc(callback), id: *self.ctx.idx.borrow(), - scope: self.ctx.scope, + scope: self.ctx.scope_ref.myidx, }; self.add_listener(listener) } @@ -354,7 +358,11 @@ where // This is okay because the bump arena is stable self.listeners.last().map(|g| { let r = unsafe { std::mem::transmute::<&Listener<'a>, &Listener<'static>>(g) }; - self.ctx.listeners.borrow_mut().push(r.callback as *const _); + self.ctx + .scope_ref + .listeners + .borrow_mut() + .push(r.callback as *const _); }); self @@ -615,6 +623,7 @@ pub fn virtual_child<'a, T: Properties + 'a>( ) -> VNode<'a> { // currently concerned about if props have a custom drop implementation // might override it with the props macro - let propsd: &'a mut _ = ctx.bump.alloc(p); + let bump = &ctx.bump(); + let propsd: &'a mut _ = bump.alloc(p); VNode::Component(crate::nodes::VComponent::new(f, propsd, key)) } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 84fc2926..0c65903a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,19 +1,37 @@ -use crate::{error::Error, innerlude::*}; -use crate::{innerlude::hooks::Hook, patch::Edit}; +//! # 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 +//! navigate the innerworkings of the Dom. We try to keep these main mechanics in this file to limit +//! the possible exposed API surface (keep fields private). This particular implementation of VDOM +//! is extremely efficient, but relies on some unsafety under the hood to do things like manage +//! micro-heaps for components. +//! +//! Included is: +//! - The [`VirtualDom`] itself +//! - The [`Scope`] object for mangning component lifecycle +//! - The [`ActiveFrame`] object for managing the Scope`s microheap +//! - The [`Context`] object for exposing VirtualDOM API to components +//! - The [`NodeCtx`] object for lazyily exposing the `Context` API to the nodebuilder API +//! - The [`Hook`] object for exposing state management in components. +//! +//! This module includes just the barebones for a complete VirtualDOM API. +//! Additional functionality is defined in the respective files. + +use crate::innerlude::*; use bumpalo::Bump; use generational_arena::Arena; use std::{ any::TypeId, - borrow::{Borrow, BorrowMut}, - cell::{Ref, RefCell, UnsafeCell}, - collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}, + cell::{RefCell, UnsafeCell}, + collections::HashSet, + fmt::Debug, + future::Future, + pin::Pin, rc::{Rc, Weak}, }; -use thiserror::private::AsDynError; -// We actually allocate the properties for components in their parent's properties -// We then expose a handle to use those props for render in the form of "OpaqueComponent" -pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + 'a; +pub use support::*; /// 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. @@ -25,30 +43,60 @@ pub struct VirtualDom { /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. components: UnsafeCell>, - /// The index of the root component.\ - /// Should always be the first + /// The index of the root component + /// Should always be the first (gen0, id0) pub base_scope: ScopeIdx, /// All components dump their updates into a queue to be processed - pub(crate) update_schedule: UpdateFunnel, + pub(crate) event_queue: EventQueue, - // a strong allocation to the "caller" for the original props + /// a strong allocation to the "caller" for the original component and its props #[doc(hidden)] - root_caller: Rc>, + _root_caller: Rc>, - // Type of the original props. This is done so VirtualDom does not need to be generic. + /// Type of the original props. This is stored as TypeId so VirtualDom does not need to be generic. + /// + /// Whenver props need to be updated, an Error will be thrown if the new props do not + /// match the props used to create the VirtualDom. #[doc(hidden)] _root_prop_type: std::any::TypeId, } // ====================================== -// Public Methods for the VirtualDOM +// Public Methods for the VirtualDom // ====================================== impl VirtualDom { /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. + /// + /// As an end-user, you'll want to use the Renderer's "new" method instead of this method. + /// Directly creating the VirtualDOM is only useful when implementing a new renderer. + /// + /// + /// ```ignore + /// // Directly from a closure + /// + /// let dom = VirtualDom::new(|ctx, _| ctx.render(rsx!{ div {"hello world"} })); + /// + /// // or pass in... + /// + /// let root = |ctx, _| { + /// ctx.render(rsx!{ + /// div {"hello world"} + /// }) + /// } + /// let dom = VirtualDom::new(root); + /// + /// // or directly from a fn + /// + /// fn Example(ctx: Context, props: &()) -> DomTree { + /// ctx.render(rsx!{ div{"hello world"} }) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -58,52 +106,80 @@ impl VirtualDom { /// /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive /// to toss out the entire tree. + /// + /// ```ignore + /// // Directly from a closure + /// + /// let dom = VirtualDom::new(|ctx, props| ctx.render(rsx!{ div {"hello world"} })); + /// + /// // or pass in... + /// + /// let root = |ctx, props| { + /// ctx.render(rsx!{ + /// div {"hello world"} + /// }) + /// } + /// let dom = VirtualDom::new(root); + /// + /// // or directly from a fn + /// + /// fn Example(ctx: Context, props: &SomeProps) -> DomTree { + /// ctx.render(rsx!{ div{"hello world"} }) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` pub fn new_with_props(root: FC

, root_props: P) -> Self { let mut components = Arena::new(); - // The user passes in a "root" component (IE the function) - // When components are used in the rsx! syntax, the parent assumes ownership - // Here, the virtual dom needs to own the function, wrapping it with a `context caller` - // The RC holds this component with a hard allocation - let root_caller: Rc = Rc::new(move |ctx| root(ctx, &root_props)); + // Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents + // Here, we need to make it manually, using an RC to force the Weak reference to stick around for the main scope. + let _root_caller: Rc = Rc::new(move |ctx| root(ctx, &root_props)); - // To make it easier to pass the root around, we just leak it - // When the virtualdom is dropped, we unleak it, so that unsafe isn't here, but it's important to remember - let leaked_caller = Rc::downgrade(&root_caller); + // Create a weak reference to the OpaqueComponent for the root scope to use as its render function + let caller_ref = Rc::downgrade(&_root_caller); + + // Build a funnel for hooks to send their updates into. The `use_hook` method will call into the update funnel. + let event_queue = EventQueue::default(); + let _event_queue = event_queue.clone(); + + // Make the first scope + // We don't run the component though, so renderers will need to call "rebuild" when they initialize their DOM + let base_scope = components + .insert_with(move |myidx| Scope::new(caller_ref, myidx, None, 0, _event_queue)); Self { - root_caller, - base_scope: components - .insert_with(move |myidx| Scope::new(leaked_caller, myidx, None, 0)), + _root_caller, + base_scope, + event_queue, components: UnsafeCell::new(components), - update_schedule: UpdateFunnel::default(), _root_prop_type: TypeId::of::

(), } } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom. from scratch pub fn rebuild<'s>(&'s mut self) -> Result> { - todo!() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct HeightMarker { - idx: ScopeIdx, - height: u32, -} -impl Ord for HeightMarker { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.height.cmp(&other.height) - } -} - -impl PartialOrd for HeightMarker { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + let mut diff_machine = DiffMachine::new(); + + // Schedule an update and then immediately call it on the root component + // This is akin to a hook being called from a listener and requring a re-render + // Instead, this is done on top-level component + unsafe { + let components = &*self.components.get(); + let base = components.get(self.base_scope).unwrap(); + let update = self.event_queue.schedule_update(base); + update(); + }; + + self.progress_completely(&mut diff_machine)?; + + Ok(diff_machine.consume()) } } +// ====================================== +// Private Methods for the VirtualDom +// ====================================== impl VirtualDom { /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. /// @@ -146,23 +222,31 @@ impl VirtualDom { // The final EditList has edits that pull directly from the Bump Arenas which add significant complexity // in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. + // + // A good project would be to remove all unsafe from this crate and move the unsafety into abstractions. pub fn progress_with_event(&mut self, event: EventTrigger) -> Result { let id = event.component_id.clone(); unsafe { (&mut *self.components.get()) .get_mut(id) - .expect("Borrowing should not fail") + .ok_or(Error::FatalInternal("Borrowing should not fail"))? .call_listener(event)?; } - // Add this component to the list of components that need to be difed let mut diff_machine = DiffMachine::new(); - let mut cur_height = 0; + self.progress_completely(&mut diff_machine)?; + + Ok(diff_machine.consume()) + } + + pub(crate) fn progress_completely(&mut self, diff_machine: &mut DiffMachine) -> Result<()> { + // Add this component to the list of components that need to be difed + let mut cur_height: u32 = 0; // Now, there are events in the queue let mut seen_nodes = HashSet::::new(); - let mut updates = self.update_schedule.0.as_ref().borrow_mut(); + let mut updates = self.event_queue.0.as_ref().borrow_mut(); // Order the nodes by their height, we want the biggest nodes on the top // This prevents us from running the same component multiple times @@ -191,9 +275,15 @@ impl VirtualDom { diff_machine.diff_node(component.old_frame(), component.next_frame()); - cur_height = component.height + 1; + cur_height = component.height; } + log::debug!( + "Processing update: {:#?} with height {}", + &update.idx, + cur_height + ); + // Now, the entire subtree has been invalidated. We need to descend depth-first and process // any updates that the diff machine has proprogated into the component lifecycle queue while let Some(event) = diff_machine.lifecycle_events.pop_front() { @@ -214,8 +304,9 @@ impl VirtualDom { let components: &mut _ = unsafe { &mut *self.components.get() }; // Insert a new scope into our component list - let idx = - components.insert_with(|f| Scope::new(caller, f, None, cur_height)); + let idx = components.insert_with(|f| { + Scope::new(caller, f, None, cur_height + 1, self.event_queue.clone()) + }); // Grab out that component let component = components.get_mut(idx).unwrap(); @@ -279,8 +370,8 @@ impl VirtualDom { // This means the caller ptr is invalidated and needs to be updated, but the component itself does not need to be re-ran LifeCycleEvent::SameProps { caller, - root_id, stable_scope_addr, + .. } => { // In this case, the parent made a new DomTree that resulted in the same props for us // However, since our caller is located in a Bump frame, we need to update the caller pointer (which is now invalid) @@ -323,38 +414,10 @@ impl VirtualDom { } } - Ok(diff_machine.consume()) + Ok(()) } } -impl Drop for VirtualDom { - fn drop(&mut self) { - // Drop all the components first - // self.components.drain(); - - // Finally, drop the root caller - unsafe { - // let root: Box> = - // Box::from_raw(self.root_caller as *const OpaqueComponent<'static> as *mut _); - - // std::mem::drop(root); - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct UpdateFunnel(Rc>>); - -impl UpdateFunnel { - fn schedule_update(&self, source: &Scope) -> impl Fn() { - let inner = self.clone(); - let marker = HeightMarker { - height: source.height, - idx: source.myidx, - }; - move || inner.0.as_ref().borrow_mut().push(marker) - } -} /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -370,11 +433,12 @@ pub struct Scope { pub height: u32, + pub event_queue: EventQueue, + // A list of children // TODO, repalce the hashset with a faster hash function pub children: HashSet, - // caller: &'static OpaqueComponent<'static>, pub caller: Weak>, // ========================== @@ -409,15 +473,24 @@ impl Scope { myidx: ScopeIdx, parent: Option, height: u32, + event_queue: EventQueue, ) -> Self { log::debug!( "New scope created, height is {}, idx is {:?}", height, myidx ); - // Caller has been broken free - // However, it's still weak, so if the original Rc gets killed, we can't touch it - let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; + + // The Componet has a lifetime that's "stuck" to its original allocation. + // We need to "break" this reference and manually manage the lifetime. + // + // Not the best solution, so TODO on removing this in favor of a dedicated resource abstraction. + let broken_caller = unsafe { + std::mem::transmute::< + Weak>, + Weak>, + >(caller) + }; Self { caller: broken_caller, @@ -428,10 +501,17 @@ impl Scope { parent, myidx, height, + event_queue, } } + pub fn update_caller<'creator_node>(&mut self, caller: Weak>) { - let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; + let broken_caller = unsafe { + std::mem::transmute::< + Weak>, + Weak>, + >(caller) + }; self.caller = broken_caller; } @@ -441,8 +521,8 @@ impl Scope { /// /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) pub fn run_scope<'b>(&'b mut self) -> Result<()> { - // cycle to the next frame and then reset it - // this breaks any latent references + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. self.frames.next().bump.reset(); let ctx = Context { @@ -451,27 +531,18 @@ impl Scope { scope: self, }; - let caller = self.caller.upgrade().expect("Failed to get caller"); + let caller = self + .caller + .upgrade() + .ok_or(Error::FatalInternal("Failed to get caller"))?; - /* - 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 - */ let new_head = unsafe { - // use the same type, just manipulate the lifetime - type ComComp<'c> = Rc>; - let caller = std::mem::transmute::, ComComp<'b>>(caller); - (caller.as_ref())(ctx) - }; + // Cast the caller ptr from static to one with our own reference + std::mem::transmute::<&OpaqueComponent<'static>, &OpaqueComponent<'b>>(caller.as_ref()) + }(ctx); self.frames.cur_frame_mut().head_node = new_head.root; + Ok(()) } @@ -480,32 +551,33 @@ impl Scope { // The listener list will be completely drained because the next frame will write over previous listeners pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { let EventTrigger { - listener_id, - event: source, - .. + listener_id, event, .. } = trigger; unsafe { // Convert the raw ptr into an actual object // This operation is assumed to be safe - - log::debug!("Running listener"); - - self.listeners - .borrow() + let listener_fn = self + .listeners + .try_borrow() + .ok() + .ok_or(Error::FatalInternal("Borrowing listener failed "))? .get(listener_id as usize) - .ok_or(Error::FatalInternal("Event should exist if it was triggered"))? + .ok_or(Error::FatalInternal("Event should exist if triggered"))? .as_ref() - .ok_or(Error::FatalInternal("Raw event ptr is invalid"))? - // Run the callback with the user event - (source); + .ok_or(Error::FatalInternal("Raw event ptr is invalid"))?; - log::debug!("Listener finished"); + // Run the callback with the user event + listener_fn(event); // drain all the event listeners // if we don't, then they'll stick around and become invalid // big big big big safety issue - self.listeners.borrow_mut().drain(..); + self.listeners + .try_borrow_mut() + .ok() + .ok_or(Error::FatalInternal("Borrowing listener failed"))? + .drain(..); } Ok(()) } @@ -523,21 +595,12 @@ impl Scope { } } -// ========================== -// Active-frame related code -// ========================== -// 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. -// It's goverened by the oldest of the two frames and is switched every time a new render occurs -// Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref -// ! do not copy this reference are things WILL break ! pub struct ActiveFrame { - pub idx: RefCell, - pub frames: [BumpFrame; 2], - // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken - pub generation: u32, + pub generation: RefCell, + + // The double-buffering situation that we will use + pub frames: [BumpFrame; 2], } pub struct BumpFrame { @@ -561,27 +624,26 @@ impl ActiveFrame { fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { Self { - idx: 0.into(), + generation: 0.into(), frames: [a, b], - generation: 0, } } fn cur_frame(&self) -> &BumpFrame { - match *self.idx.borrow() & 1 == 0 { + match *self.generation.borrow() & 1 == 0 { true => &self.frames[0], false => &self.frames[1], } } fn cur_frame_mut(&mut self) -> &mut BumpFrame { - match *self.idx.borrow() & 1 == 0 { + match *self.generation.borrow() & 1 == 0 { true => &mut self.frames[0], false => &mut self.frames[1], } } pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.idx.borrow() & 1 == 0 { + let raw_node = match *self.generation.borrow() & 1 == 0 { true => &self.frames[0], false => &self.frames[1], }; @@ -595,7 +657,7 @@ impl ActiveFrame { } pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.idx.borrow() & 1 != 0 { + let raw_node = match *self.generation.borrow() & 1 != 0 { true => &self.frames[0], false => &self.frames[1], }; @@ -609,9 +671,9 @@ impl ActiveFrame { } fn next(&mut self) -> &mut BumpFrame { - *self.idx.borrow_mut() += 1; + *self.generation.borrow_mut() += 1; - if *self.idx.borrow() % 2 == 0 { + if *self.generation.borrow() % 2 == 0 { &mut self.frames[0] } else { &mut self.frames[1] @@ -619,7 +681,218 @@ impl ActiveFrame { } } -mod test { +/// Components in Dioxus use the "Context" object to interact with their lifecycle. +/// This lets components schedule updates, integrate hooks, and expose their context via the context api. +/// +/// Properties passed down from the parent component are also directly accessible via the exposed "props" field. +/// +/// ```ignore +/// #[derive(Properties)] +/// struct Props { +/// name: String +/// +/// } +/// +/// fn example(ctx: Context, props: &Props -> VNode { +/// html! { +///

"Hello, {ctx.props.name}"
+/// } +/// } +/// ``` +// todo: force lifetime of source into T as a valid lifetime too +// it's definitely possible, just needs some more messing around +pub struct Context<'src> { + pub idx: RefCell, + + // pub scope: ScopeIdx, + pub scope: &'src Scope, + + pub _p: std::marker::PhantomData<&'src ()>, +} + +impl<'a> Context<'a> { + /// Access the children elements passed into the component + pub fn children(&self) -> Vec { + todo!("Children API not yet implemented for component Context") + } + + pub fn callback(&self, _f: impl Fn(()) + 'a) {} + + /// Create a subscription that schedules a future render for the reference component + pub fn schedule_update(&self) -> impl Fn() -> () { + self.scope.event_queue.schedule_update(&self.scope) + } + + /// Create a suspended component from a future. + /// + /// When the future completes, the component will be renderered + pub fn suspend FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>( + &self, + _fut: impl Future>, + ) -> VNode<'a> { + todo!() + } +} + +impl<'scope> Context<'scope> { + /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. + /// + /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. + /// + /// ## Example + /// + /// ```ignore + /// fn Component(ctx: Context) -> VNode { + /// // Lazy assemble the VNode tree + /// let lazy_tree = html! {
"Hello World"
}; + /// + /// // Actually build the tree and allocate it + /// ctx.render(lazy_tree) + /// } + ///``` + pub fn render FnOnce(&'b NodeCtx<'scope>) -> VNode<'scope> + 'scope>( + &self, + lazy_nodes: LazyNodes<'scope, F>, + ) -> DomTree { + let ctx = NodeCtx { + scope_ref: self.scope, + idx: 0.into(), + }; + + let safe_nodes: VNode<'scope> = lazy_nodes.into_vnode(&ctx); + let root: VNode<'static> = unsafe { std::mem::transmute(safe_nodes) }; + DomTree { root } + } +} + +type Hook = Pin>; + +impl<'scope> Context<'scope> { + /// Store a value between renders + /// + /// - Initializer: closure used to create the initial hook state + /// - Runner: closure used to output a value every time the hook is used + /// - Cleanup: closure used to teardown the hook once the dom is cleaned up + /// + /// ```ignore + /// // use_ref is the simplest way of storing a value between renders + /// pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> { + /// use_hook( + /// || Rc::new(RefCell::new(initial_value())), + /// |state, _| state.clone(), + /// |_| {}, + /// ) + /// } + /// ``` + pub fn use_hook<'c, InternalHookState: 'static, Output: 'scope>( + &'c self, + + // The closure that builds the hook state + initializer: impl FnOnce() -> InternalHookState, + + // The closure that takes the hookstate and returns some value + runner: impl FnOnce(&'scope mut InternalHookState) -> Output, + + // The closure that cleans up whatever mess is left when the component gets torn down + // TODO: add this to the "clean up" group for when the component is dropped + _cleanup: impl FnOnce(InternalHookState), + ) -> Output { + let idx = *self.idx.borrow(); + + // Grab out the hook list + let mut hooks = self.scope.hooks.borrow_mut(); + + // If the idx is the same as the hook length, then we need to add the current hook + if idx >= hooks.len() { + let new_state = initializer(); + hooks.push(Box::pin(new_state)); + } + + *self.idx.borrow_mut() += 1; + + let stable_ref = hooks + .get_mut(idx) + .expect("Should not fail, idx is validated") + .as_mut(); + + let pinned_state = unsafe { Pin::get_unchecked_mut(stable_ref) }; + + let internal_state = pinned_state.downcast_mut::().expect( + r###" + Unable to retrive the hook that was initialized in this index. + Consult the `rules of hooks` to understand how to use hooks properly. + + You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. + "###, + ); + + // We extend the lifetime of the internal state + runner(unsafe { &mut *(internal_state as *mut _) }) + } +} + +mod support { + use super::*; + + // We actually allocate the properties for components in their parent's properties + // We then expose a handle to use those props for render in the form of "OpaqueComponent" + pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + 'a; + + #[derive(Debug, Default, Clone)] + pub struct EventQueue(pub(crate) Rc>>); + + impl EventQueue { + pub fn schedule_update(&self, source: &Scope) -> impl Fn() { + let inner = self.clone(); + let marker = HeightMarker { + height: source.height, + idx: source.myidx, + }; + move || inner.0.as_ref().borrow_mut().push(marker) + } + } + + /// A helper type that lets scopes be ordered by their height + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct HeightMarker { + pub idx: ScopeIdx, + pub height: u32, + } + + impl Ord for HeightMarker { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.height.cmp(&other.height) + } + } + + impl PartialOrd for HeightMarker { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + // NodeCtx is used to build VNodes in the component's memory space. + // This struct adds metadata to the final DomTree about listeners, attributes, and children + #[derive(Clone)] + pub struct NodeCtx<'a> { + pub scope_ref: &'a Scope, + pub idx: RefCell, + } + + impl<'a> NodeCtx<'a> { + pub fn bump(&self) -> &'a Bump { + &self.scope_ref.cur_frame().bump + } + } + + impl Debug for NodeCtx<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + } +} + +mod tests { use super::*; #[test] @@ -634,12 +907,4 @@ mod test { }); // let root = dom.components.get(dom.base_scope).unwrap(); } - - // // This marker is designed to ensure resources shared from one bump to another are handled properly - // // The underlying T may be already freed if there is an issue with our crate - // pub(crate) struct BumpResource { - // resource: T, - // scope: ScopeIdx, - // gen: u32, - // } } diff --git a/packages/web/examples/rsxt.rs b/packages/web/examples/rsxt.rs index 04419bc6..0adfcbcb 100644 --- a/packages/web/examples/rsxt.rs +++ b/packages/web/examples/rsxt.rs @@ -22,7 +22,7 @@ struct ExampleProps { } static Example: FC = |ctx, props| { - let name = use_state_new(&ctx, move || props.initial_name.to_string()); + let name = use_state_new(&ctx, move || props.initial_name); ctx.render(rsx! { div { @@ -36,9 +36,11 @@ static Example: FC = |ctx, props| { "Hello, {name}" } - CustomButton { name: "Jack!", handler: move |_| name.set("Jack".to_string()) } - CustomButton { name: "Jill!", handler: move |_| name.set("Jill".to_string()) } - CustomButton { name: "Bob!", handler: move |_| name.set("Bob".to_string())} + CustomButton { name: "Jack!", handler: move |_| name.set("Jack") } + CustomButton { name: "Jill!", handler: move |_| name.set("Jill") } + CustomButton { name: "Bob!", handler: move |_| name.set("Bob")} + Placeholder {val: name} + Placeholder {val: name} } }) }; @@ -66,3 +68,16 @@ impl PartialEq for ButtonProps<'_, F> { false } } + + +#[derive(Props, PartialEq)] +struct PlaceholderProps { + val: &'static str +} +fn Placeholder(ctx: Context, props: &PlaceholderProps) -> DomTree { + ctx.render(rsx!{ + div { + "child: {props.val}" + } + }) +}