1
0
Эх сурвалжийг харах

wip: think about dyn any for ScopeState

Jonathan Kelley 2 жил өмнө
parent
commit
12808ec0aa

+ 0 - 11
packages/core/src/events.rs

@@ -158,14 +158,3 @@ impl<T> EventHandler<'_, T> {
         self.callback.replace(None);
         self.callback.replace(None);
     }
     }
 }
 }
-
-#[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]));
-}

+ 1 - 1
packages/core/src/scope_arena.rs

@@ -36,7 +36,7 @@ impl VirtualDom {
             spawned_tasks: Default::default(),
             spawned_tasks: Default::default(),
             render_cnt: Default::default(),
             render_cnt: Default::default(),
             hook_arena: Default::default(),
             hook_arena: Default::default(),
-            hook_vals: Default::default(),
+            hook_list: Default::default(),
             hook_idx: Default::default(),
             hook_idx: Default::default(),
             shared_contexts: Default::default(),
             shared_contexts: Default::default(),
             tasks: self.scheduler.clone(),
             tasks: self.scheduler.clone(),

+ 87 - 116
packages/core/src/scopes.rs

@@ -1,14 +1,3 @@
-use std::{
-    any::{Any, TypeId},
-    cell::{Cell, RefCell},
-    collections::{HashMap, HashSet},
-    rc::Rc,
-    sync::Arc,
-};
-
-use bumpalo::Bump;
-use std::future::Future;
-
 use crate::{
 use crate::{
     any_props::AnyProps,
     any_props::AnyProps,
     arena::ElementId,
     arena::ElementId,
@@ -18,17 +7,40 @@ use crate::{
     nodes::VNode,
     nodes::VNode,
     TaskId,
     TaskId,
 };
 };
+use bumpalo::Bump;
+use std::future::Future;
+use std::{
+    any::{Any, TypeId},
+    cell::{Cell, RefCell},
+    collections::{HashMap, HashSet},
+    rc::Rc,
+    sync::Arc,
+};
 
 
+/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given
+/// component.
+///
+/// The [`Scope`] is your handle to the [`VirtualDom`] and the component state. Every component is given its own
+/// [`ScopeState`] and merged with its properties to create a [`Scoped`].
+///
+/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the
+/// component render.
 pub type Scope<'a, T = ()> = &'a Scoped<'a, T>;
 pub type Scope<'a, T = ()> = &'a Scoped<'a, T>;
 
 
+/// A wrapper around a component's [`ScopeState`] and properties. The [`ScopeState`] provides the majority of methods
+/// for the VirtualDom and component state.
 pub struct Scoped<'a, T = ()> {
 pub struct Scoped<'a, T = ()> {
+    /// The component's state and handle to the scheduler.
+    ///
+    /// Stores things like the custom bump arena, spawn functions, hooks, and the scheduler.
     pub scope: &'a ScopeState,
     pub scope: &'a ScopeState,
+
+    /// The component's properties.
     pub props: &'a T,
     pub props: &'a T,
 }
 }
 
 
 impl<'a, T> std::ops::Deref for Scoped<'a, T> {
 impl<'a, T> std::ops::Deref for Scoped<'a, T> {
     type Target = &'a ScopeState;
     type Target = &'a ScopeState;
-
     fn deref(&self) -> &Self::Target {
     fn deref(&self) -> &Self::Target {
         &self.scope
         &self.scope
     }
     }
@@ -42,6 +54,9 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
 pub struct ScopeId(pub usize);
 pub struct ScopeId(pub usize);
 
 
+/// A component's state.
+///
+/// This struct stores all the important information about a component's state without the props.
 pub struct ScopeState {
 pub struct ScopeState {
     pub(crate) render_cnt: Cell<usize>,
     pub(crate) render_cnt: Cell<usize>,
 
 
@@ -55,7 +70,7 @@ pub struct ScopeState {
     pub(crate) height: u32,
     pub(crate) height: u32,
 
 
     pub(crate) hook_arena: Bump,
     pub(crate) hook_arena: Bump,
-    pub(crate) hook_vals: RefCell<Vec<*mut dyn Any>>,
+    pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
     pub(crate) hook_idx: Cell<usize>,
     pub(crate) hook_idx: Cell<usize>,
 
 
     pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
     pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
@@ -75,6 +90,7 @@ impl ScopeState {
             _ => unreachable!(),
             _ => unreachable!(),
         }
         }
     }
     }
+
     pub fn previous_frame(&self) -> &BumpFrame {
     pub fn previous_frame(&self) -> &BumpFrame {
         match self.render_cnt.get() % 2 {
         match self.render_cnt.get() % 2 {
             1 => &self.node_arena_1,
             1 => &self.node_arena_1,
@@ -83,10 +99,19 @@ impl ScopeState {
         }
         }
     }
     }
 
 
+    /// Get a handle to the currently active bump arena for this Scope
+    ///
+    /// This is a bump memory allocator. Be careful using this directly since the contents will be wiped on the next render.
+    /// It's easy to leak memory here since the drop implementation will not be called for any objects allocated in this arena.
+    ///
+    /// If you need to allocate items that need to be dropped, use bumpalo's box.
     pub fn bump(&self) -> &Bump {
     pub fn bump(&self) -> &Bump {
         &self.current_frame().bump
         &self.current_frame().bump
     }
     }
 
 
+    /// Get a handle to the currently active head node arena for this Scope
+    ///
+    /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
     pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
     pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
         let r = unsafe { &*self.current_frame().node.get() };
         let r = unsafe { &*self.current_frame().node.get() };
         unsafe { std::mem::transmute(r) }
         unsafe { std::mem::transmute(r) }
@@ -166,6 +191,7 @@ impl ScopeState {
         Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
         Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
     }
     }
 
 
+    /// Mark this scope as dirty, and schedule a render for it.
     pub fn needs_update(&self) {
     pub fn needs_update(&self) {
         self.needs_update_any(self.scope_id());
         self.needs_update_any(self.scope_id());
     }
     }
@@ -180,6 +206,42 @@ impl ScopeState {
             .expect("Scheduler to exist if scope exists");
             .expect("Scheduler to exist if scope exists");
     }
     }
 
 
+    /// Return any context of type T if it exists on this scope
+    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
+        self.shared_contexts
+            .borrow()
+            .get(&TypeId::of::<T>())
+            .and_then(|shared| shared.downcast_ref::<T>())
+            .cloned()
+    }
+
+    /// Try to retrieve a shared state with type `T` from any parent scope.
+    ///
+    /// The state will be cloned and returned, if it exists.
+    ///
+    /// We recommend wrapping the state in an `Rc` or `Arc` to avoid deep cloning.
+    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
+        if let Some(this_ctx) = self.has_context() {
+            return Some(this_ctx);
+        }
+
+        let mut search_parent = self.parent;
+        while let Some(parent_ptr) = search_parent {
+            // safety: all parent pointers are valid thanks to the bump arena
+            let parent = unsafe { &*parent_ptr };
+            if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
+                return Some(
+                    shared
+                        .downcast_ref::<T>()
+                        .expect("Context of type T should exist")
+                        .clone(),
+                );
+            }
+            search_parent = parent.parent;
+        }
+        None
+    }
+
     /// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree.
     /// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree.
     ///
     ///
     /// This is a "fundamental" operation and should only be called during initialization of a hook.
     /// This is a "fundamental" operation and should only be called during initialization of a hook.
@@ -212,96 +274,6 @@ impl ScopeState {
         value
         value
     }
     }
 
 
-    /// Provide a context for the root component from anywhere in your app.
-    ///
-    ///
-    /// # Example
-    ///
-    /// ```rust, ignore
-    /// struct SharedState(&'static str);
-    ///
-    /// static App: Component = |cx| {
-    ///     cx.use_hook(|| cx.provide_root_context(SharedState("world")));
-    ///     render!(Child {})
-    /// }
-    ///
-    /// static Child: Component = |cx| {
-    ///     let state = cx.consume_state::<SharedState>();
-    ///     render!(div { "hello {state.0}" })
-    /// }
-    /// ```
-    pub fn provide_root_context<T: 'static + Clone>(&self, value: T) -> T {
-        // if we *are* the root component, then we can just provide the context directly
-        if self.scope_id() == ScopeId(0) {
-            self.shared_contexts
-                .borrow_mut()
-                .insert(TypeId::of::<T>(), Box::new(value.clone()))
-                .and_then(|f| f.downcast::<T>().ok());
-            return value;
-        }
-
-        let mut search_parent = self.parent;
-
-        while let Some(parent) = search_parent.take() {
-            let parent = unsafe { &*parent };
-
-            if parent.scope_id() == ScopeId(0) {
-                let _ = parent
-                    .shared_contexts
-                    .borrow_mut()
-                    .insert(TypeId::of::<T>(), Box::new(value.clone()));
-
-                return value;
-            }
-
-            search_parent = parent.parent;
-        }
-
-        unreachable!("all apps have a root scope")
-    }
-
-    /// Try to retrieve a shared state with type T from the any parent Scope.
-    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
-        if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-            Some(
-                (*shared
-                    .downcast_ref::<T>()
-                    .expect("Context of type T should exist"))
-                .clone(),
-            )
-        } else {
-            let mut search_parent = self.parent;
-
-            while let Some(parent_ptr) = search_parent {
-                // safety: all parent pointers are valid thanks to the bump arena
-                let parent = unsafe { &*parent_ptr };
-                if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-                    return Some(
-                        shared
-                            .downcast_ref::<T>()
-                            .expect("Context of type T should exist")
-                            .clone(),
-                    );
-                }
-                search_parent = parent.parent;
-            }
-            None
-        }
-    }
-
-    /// Return any context of type T if it exists on this scope
-    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
-        match self.shared_contexts.borrow().get(&TypeId::of::<T>()) {
-            Some(shared) => Some(
-                (*shared
-                    .downcast_ref::<T>()
-                    .expect("Context of type T should exist"))
-                .clone(),
-            ),
-            None => None,
-        }
-    }
-
     /// Pushes the future onto the poll queue to be polled after the component renders.
     /// Pushes the future onto the poll queue to be polled after the component renders.
     pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
     pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
         self.tasks.spawn(self.id, fut)
         self.tasks.spawn(self.id, fut)
@@ -312,7 +284,7 @@ impl ScopeState {
         self.push_future(fut);
         self.push_future(fut);
     }
     }
 
 
-    /// Spawn a future that Dioxus will never clean up
+    /// Spawn a future that Dioxus won't clean up when this component is unmounted
     ///
     ///
     /// This is good for tasks that need to be run after the component has been dropped.
     /// This is good for tasks that need to be run after the component has been dropped.
     pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
     pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
@@ -328,13 +300,14 @@ impl ScopeState {
         id
         id
     }
     }
 
 
-    /// Informs the scheduler that this task is no longer needed and should be removed
-    /// on next poll.
+    /// Informs the scheduler that this task is no longer needed and should be removed.
+    ///
+    /// This drops the task immediately.
     pub fn remove_future(&self, id: TaskId) {
     pub fn remove_future(&self, id: TaskId) {
         self.tasks.remove(id);
         self.tasks.remove(id);
     }
     }
 
 
-    /// Take a lazy [`VNode`] structure and actually build it with the context of the Vdoms efficient [`VNode`] allocator.
+    /// Take a lazy [`VNode`] structure and actually build it with the context of the efficient [`Bump`] allocator.
     ///
     ///
     /// ## Example
     /// ## Example
     ///
     ///
@@ -369,19 +342,17 @@ impl ScopeState {
     /// ```
     /// ```
     #[allow(clippy::mut_from_ref)]
     #[allow(clippy::mut_from_ref)]
     pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
     pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
-        let mut vals = self.hook_vals.borrow_mut();
-
-        let hook_len = vals.len();
-        let cur_idx = self.hook_idx.get();
+        let cur_hook = self.hook_idx.get();
+        let mut hook_list = self.hook_list.borrow_mut();
 
 
-        if cur_idx >= hook_len {
-            vals.push(self.hook_arena.alloc(initializer()));
+        if cur_hook >= hook_list.len() {
+            hook_list.push(self.hook_arena.alloc(initializer()));
         }
         }
 
 
-        vals
-            .get(cur_idx)
+        hook_list
+            .get(cur_hook)
             .and_then(|inn| {
             .and_then(|inn| {
-                self.hook_idx.set(cur_idx + 1);
+                self.hook_idx.set(cur_hook + 1);
                 let raw_box = unsafe { &mut **inn };
                 let raw_box = unsafe { &mut **inn };
                 raw_box.downcast_mut::<State>()
                 raw_box.downcast_mut::<State>()
             })
             })

+ 29 - 10
packages/core/src/virtual_dom.rs

@@ -1,3 +1,7 @@
+//! # Virtual DOM Implementation for Rust
+//!
+//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
+
 use crate::{
 use crate::{
     any_props::VComponentProps,
     any_props::VComponentProps,
     arena::ElementId,
     arena::ElementId,
@@ -13,12 +17,13 @@ use crate::{
 };
 };
 use futures_util::{pin_mut, StreamExt};
 use futures_util::{pin_mut, StreamExt};
 use slab::Slab;
 use slab::Slab;
-use std::rc::Rc;
 use std::{
 use std::{
     any::Any,
     any::Any,
+    cell::Cell,
     collections::{BTreeSet, HashMap},
     collections::{BTreeSet, HashMap},
+    future::Future,
+    rc::Rc,
 };
 };
-use std::{cell::Cell, future::Future};
 
 
 /// A virtual node system that progresses user events and diffs UI trees.
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
 ///
@@ -245,9 +250,11 @@ impl VirtualDom {
 
 
         // The root component is always a suspense boundary for any async children
         // The root component is always a suspense boundary for any async children
         // This could be unexpected, so we might rethink this behavior later
         // This could be unexpected, so we might rethink this behavior later
+        //
+        // We *could* just panic if the suspense boundary is not found
         root.provide_context(SuspenseBoundary::new(ScopeId(0)));
         root.provide_context(SuspenseBoundary::new(ScopeId(0)));
 
 
-        // the root element is always given element 0
+        // the root element is always given element ID 0 since it's the container for the entire tree
         dom.elements.insert(ElementRef::null());
         dom.elements.insert(ElementRef::null());
 
 
         dom
         dom
@@ -269,19 +276,24 @@ impl VirtualDom {
 
 
     /// Build the virtualdom with a global context inserted into the base scope
     /// Build the virtualdom with a global context inserted into the base scope
     ///
     ///
-    /// This is useful for what is essentially dependency injection, when building the app
+    /// This is useful for what is essentially dependency injection when building the app
     pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
     pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
         self.base_scope().provide_context(context);
         self.base_scope().provide_context(context);
         self
         self
     }
     }
 
 
     /// Manually mark a scope as requiring a re-render
     /// Manually mark a scope as requiring a re-render
+    ///
+    /// Whenever the VirtualDom "works", it will re-render this scope
     pub fn mark_dirty_scope(&mut self, id: ScopeId) {
     pub fn mark_dirty_scope(&mut self, id: ScopeId) {
         let height = self.scopes[id.0].height;
         let height = self.scopes[id.0].height;
         self.dirty_scopes.insert(DirtyScope { height, id });
         self.dirty_scopes.insert(DirtyScope { height, id });
     }
     }
 
 
     /// Determine whether or not a scope is currently in a suspended state
     /// Determine whether or not a scope is currently in a suspended state
+    ///
+    /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
+    /// currently suspended.
     pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
     pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
         !self.scopes[id.0]
         !self.scopes[id.0]
             .consume_context::<SuspenseContext>()
             .consume_context::<SuspenseContext>()
@@ -291,7 +303,7 @@ impl VirtualDom {
             .is_empty()
             .is_empty()
     }
     }
 
 
-    /// Determine is the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
+    /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
     /// ready to be rendered.
     /// ready to be rendered.
     pub fn has_suspended_work(&self) -> bool {
     pub fn has_suspended_work(&self) -> bool {
         !self.scheduler.leaves.borrow().is_empty()
         !self.scheduler.leaves.borrow().is_empty()
@@ -324,6 +336,10 @@ impl VirtualDom {
 
 
         If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
         If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
         ----------------------
         ----------------------
+
+        For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the
+        right.
+
         |           <-- yes (is ascendant)
         |           <-- yes (is ascendant)
         | | |       <-- no  (is not direct ascendant)
         | | |       <-- no  (is not direct ascendant)
         | |         <-- yes (is ascendant)
         | |         <-- yes (is ascendant)
@@ -354,6 +370,7 @@ impl VirtualDom {
                 let this_path = template.template.attr_paths[idx];
                 let this_path = template.template.attr_paths[idx];
 
 
                 // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
                 // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
+                // we should fix this so that we look for "onclick" instead of "click"
                 if &attr.name[2..] == name && is_path_ascendant(&target_path, &this_path) {
                 if &attr.name[2..] == name && is_path_ascendant(&target_path, &this_path) {
                     listeners.push(&attr.value);
                     listeners.push(&attr.value);
 
 
@@ -362,7 +379,9 @@ impl VirtualDom {
                         break;
                         break;
                     }
                     }
 
 
-                    // Break if this is the exact target element
+                    // Break if this is the exact target element.
+                    // This means we won't call two listeners with the same name on the same element. This should be
+                    // documented, or be rejected from the rsx! macro outright
                     if this_path == target_path {
                     if this_path == target_path {
                         break;
                         break;
                     }
                     }
@@ -392,7 +411,7 @@ impl VirtualDom {
     ///
     ///
     /// This method is cancel-safe, so you're fine to discard the future in a select block.
     /// This method is cancel-safe, so you're fine to discard the future in a select block.
     ///
     ///
-    /// This lets us poll async tasks during idle periods without blocking the main thread.
+    /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread.
     ///
     ///
     /// # Example
     /// # Example
     ///
     ///
@@ -433,7 +452,7 @@ impl VirtualDom {
 
 
     /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
     /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
     ///
     ///
-    /// The diff machine expects the RealDom's stack to be the root of the application.
+    /// The mutations item expects the RealDom's stack to be the root of the application.
     ///
     ///
     /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
     /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
     /// root component will be ran once and then diffed. All updates will flow out as mutations.
     /// root component will be ran once and then diffed. All updates will flow out as mutations.
@@ -498,7 +517,6 @@ impl VirtualDom {
         &'a mut self,
         &'a mut self,
         deadline: impl Future<Output = ()>,
         deadline: impl Future<Output = ()>,
     ) -> Mutations<'a> {
     ) -> Mutations<'a> {
-        use futures_util::future::{select, Either};
         pin_mut!(deadline);
         pin_mut!(deadline);
 
 
         let mut mutations = Mutations::new(0);
         let mut mutations = Mutations::new(0);
@@ -533,7 +551,7 @@ impl VirtualDom {
 
 
             // Wait for suspense, or a deadline
             // Wait for suspense, or a deadline
             if self.dirty_scopes.is_empty() {
             if self.dirty_scopes.is_empty() {
-                // If there's no suspense, then we have no reason to wait
+                // If there's no pending suspense, then we have no reason to wait
                 if self.scheduler.leaves.borrow().is_empty() {
                 if self.scheduler.leaves.borrow().is_empty() {
                     return mutations;
                     return mutations;
                 }
                 }
@@ -543,6 +561,7 @@ impl VirtualDom {
                 pin_mut!(work);
                 pin_mut!(work);
 
 
                 // If the deadline is exceded (left) then we should return the mutations we have
                 // If the deadline is exceded (left) then we should return the mutations we have
+                use futures_util::future::{select, Either};
                 if let Either::Left((_, _)) = select(&mut deadline, work).await {
                 if let Either::Left((_, _)) = select(&mut deadline, work).await {
                     return mutations;
                     return mutations;
                 }
                 }

+ 2 - 1
packages/core/tests/task.rs

@@ -1,6 +1,7 @@
-use std::time::Duration;
+//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
 
 
 use dioxus_core::*;
 use dioxus_core::*;
+use std::time::Duration;
 
 
 #[tokio::test]
 #[tokio::test]
 async fn it_works() {
 async fn it_works() {