From d90c71c508bf6d3d248c6dd668da14d11a4a74a1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 5 Jan 2024 13:18:48 -0600 Subject: [PATCH] move creation functions into the structs they are creating and unify user and system events --- packages/core/src/arena.rs | 5 +- packages/core/src/create.rs | 4 - packages/core/src/diff.rs | 1 - packages/core/src/events.rs | 15 +- packages/core/src/nodes.rs | 90 +++++++-- packages/core/src/scope_context.rs | 5 + packages/core/src/scopes.rs | 286 +---------------------------- packages/core/src/virtual_dom.rs | 23 +-- 8 files changed, 110 insertions(+), 319 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 02e3997b6..9dc01f880 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -21,9 +21,6 @@ pub struct ElementRef { // the pathway of the real element inside the template pub(crate) path: ElementPath, - // the scope that this element belongs to - pub(crate) scope: ScopeId, - // The actual element pub(crate) element: VNode, } @@ -59,7 +56,7 @@ impl VirtualDom { // Note: This will not remove any ids from the arena pub(crate) fn drop_scope(&mut self, id: ScopeId) { self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[id.0].height(), + height: self.scopes[id.0].context().height, id, }); diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index e8f0b2b18..d8e97981d 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -199,7 +199,6 @@ impl VirtualDom { path: template.template.get().node_paths[idx], }, element: template.clone(), - scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), }; self.create_dynamic_node(&template_ref, node, to) } @@ -209,7 +208,6 @@ impl VirtualDom { path: template.template.get().node_paths[idx], }, element: template.clone(), - scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), }; *parent.borrow_mut() = Some(template_ref); let id = self.set_slot(id); @@ -297,7 +295,6 @@ impl VirtualDom { path: template.template.get().node_paths[idx], }, element: template.clone(), - scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), }; let m = self.create_dynamic_node(&boundary_ref, &template.dynamic_nodes[idx], to); if m > 0 { @@ -353,7 +350,6 @@ impl VirtualDom { let element_ref = ElementRef { path: ElementPath { path }, element: template.clone(), - scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), }; self.elements[id.0] = Some(element_ref); to.create_event_listener(&unbounded_name[2..], id); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index bb85211e7..b9a6bb5c7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -142,7 +142,6 @@ impl VirtualDom { path: ElementPath { path: left_template.template.get().node_paths[dyn_node_idx], }, - scope: self.runtime.scope_stack.borrow().last().copied().unwrap(), }; self.diff_dynamic_node(left_node, right_node, ¤t_ref, to); }); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 7e208ba35..5cd4665e9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,4 +1,4 @@ -use crate::{runtime::with_runtime, ScopeId}; +use crate::{runtime::with_runtime, scope_context::current_scope_id, ScopeId}; use std::{ cell::{Cell, RefCell}, rc::Rc, @@ -149,7 +149,18 @@ impl Default for EventHandler { type ExternalListenerCallback = Box; -impl EventHandler< T> { +impl EventHandler { + /// Create a new [`EventHandler`] from an [`FnMut`] + pub fn new(mut f: impl FnMut(T) + 'static) -> EventHandler { + let callback = RefCell::new(Some(Box::new(move |event: T| { + f(event); + }) as Box)); + EventHandler { + callback, + origin: current_scope_id().expect("to be in a dioxus runtime"), + } + } + /// Call this event handler with the appropriate event type /// /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 8b39dad2d..f48609edf 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,5 +1,6 @@ -use crate::any_props::BoxedAnyProps; -use crate::innerlude::ElementRef; +use crate::any_props::{BoxedAnyProps, VProps}; +use crate::innerlude::{ElementRef, EventHandler}; +use crate::Properties; use crate::{arena::ElementId, Element, Event, ScopeId}; use std::ops::Deref; use std::rc::Rc; @@ -336,6 +337,13 @@ pub enum DynamicNode { Fragment(Vec), } +impl DynamicNode { + /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] + pub fn make_node<'c, I>(into: impl IntoDynNode + 'c) -> DynamicNode { + into.into_dyn_node() + } +} + impl Default for DynamicNode { fn default() -> Self { Self::Placeholder(Default::default()) @@ -360,6 +368,35 @@ pub struct VComponent { } impl VComponent { + /// Create a new [`VComponent`] variant + /// + /// + /// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result`]. + /// + /// ```rust, ignore + /// // Without explicit props + /// fn(Scope) -> Element; + /// async fn(Scope<'_>) -> Element; + /// + /// // With explicit props + /// fn(Scope) -> Element; + /// async fn(Scope>) -> Element; + /// ``` + pub fn new

(&self, component: fn(P) -> Element, props: P, fn_name: &'static str) -> Self + where + // The properties must be valid until the next bump frame + P: Properties, + { + let vcomp = VProps::new(component, P::memoize, props, fn_name); + + VComponent { + name: fn_name, + render_fn: component as *const (), + props: BoxedAnyProps::new(vcomp), + scope: Default::default(), + } + } + /// Get the scope that this component is mounted to pub fn mounted_scope(&self) -> Option { self.scope.get() @@ -400,6 +437,12 @@ impl VText { } } +impl From> for VText { + fn from(args: Arguments) -> Self { + Self::new(args.to_string()) + } +} + /// A placeholder node, used by suspense and fragments #[derive(Clone, Debug, Default)] pub struct VPlaceholder { @@ -471,19 +514,23 @@ pub struct Attribute { } impl Attribute { - /// Create a new attribute - pub fn new( + /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool + /// + /// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in + /// some renderers stay in sync with the VirtualDom's understanding of the world + pub fn attr( + &self, name: &'static str, - value: AttributeValue, + value: impl IntoAttributeValue, namespace: Option<&'static str>, volatile: bool, - ) -> Self { - Self { + ) -> Attribute { + Attribute { name, - value, namespace, volatile, - mounted_element: Cell::new(ElementId::default()), + mounted_element: Default::default(), + value: value.into_value(), } } @@ -511,7 +558,7 @@ pub enum AttributeValue { Bool(bool), /// A listener, like "onclick" - Listener(RefCell), + Listener(ListenerCb), /// An arbitrary value that implements PartialEq and is static Any(Box), @@ -520,7 +567,28 @@ pub enum AttributeValue { None, } -pub type ListenerCb = Box)>; +impl AttributeValue { + /// Create a new [`AttributeValue`] with the listener variant from a callback + /// + /// The callback must be confined to the lifetime of the ScopeState + pub fn listener(mut callback: impl FnMut(Event) + 'static) -> AttributeValue { + AttributeValue::Listener(EventHandler::new(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propagates: event.propagates, + data, + }); + } + })) + } + + /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] + pub fn any_value(&self, value: T) -> AttributeValue { + AttributeValue::Any(Box::new(value)) + } +} + +pub type ListenerCb = EventHandler>; impl std::fmt::Debug for AttributeValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 9fc6caad8..7a8039f37 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -301,6 +301,11 @@ impl ScopeContext { pub fn generation(&self) -> usize { self.render_count.get() } + + /// Get the height of this scope + pub fn height(&self) -> u32 { + self.height + } } impl Drop for ScopeContext { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 958aa8ee1..da8acb619 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,19 +1,7 @@ use crate::{ - any_props::{BoxedAnyProps, VProps}, - innerlude::{DynamicNode, EventHandler, VComponent, VText}, - nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, - runtime::Runtime, - scope_context::ScopeContext, - AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, -}; -use std::{ - any::Any, - cell::{Ref, RefCell}, - fmt::{Arguments, Debug}, - future::Future, - rc::Rc, - sync::Arc, + any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::ScopeContext, }; +use std::{cell::Ref, fmt::Debug, rc::Rc}; /// A component's unique identifier. /// @@ -57,16 +45,11 @@ impl Drop for ScopeState { } } -impl<'src> ScopeState { +impl ScopeState { pub(crate) fn context(&self) -> Ref<'_, ScopeContext> { self.runtime.get_context(self.context_id).unwrap() } - /// Get the name of this component - pub fn name(&self) -> &str { - self.context().name - } - /// 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. @@ -85,267 +68,4 @@ impl<'src> ScopeState { pub fn try_root_node(&self) -> Option<&RenderReturn> { self.last_rendered_node.as_ref() } - - /// Get the height of this Scope - IE the number of scopes above it. - /// - /// A Scope with a height of `0` is the root scope - there are no other scopes above it. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); - /// dom.rebuild(); - /// - /// let base = dom.base_scope(); - /// - /// assert_eq!(base.height(), 0); - /// ``` - pub fn height(&self) -> u32 { - self.context().height - } - - /// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`]. - /// - /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. - /// - /// The base component will not have a parent, and will return `None`. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); - /// dom.rebuild(); - /// - /// let base = dom.base_scope(); - /// - /// assert_eq!(base.parent(), None); - /// ``` - pub fn parent(&self) -> Option { - // safety: the pointer to our parent is *always* valid thanks to the bump arena - self.context().parent_id() - } - - /// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`]. - /// - /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); - /// dom.rebuild(); - /// let base = dom.base_scope(); - /// - /// assert_eq!(base.scope_id(), 0); - /// ``` - pub fn scope_id(&self) -> ScopeId { - self.context().scope_id() - } - - /// Create a subscription that schedules a future render for the reference component - /// - /// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`] - pub fn schedule_update(&self) -> Arc { - self.context().schedule_update() - } - - /// Schedule an update for any component given its [`ScopeId`]. - /// - /// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method. - /// - /// This method should be used when you want to schedule an update for a component - pub fn schedule_update_any(&self) -> Arc { - self.context().schedule_update_any() - } - - /// Mark this scope as dirty, and schedule a render for it. - pub fn needs_update(&self) { - self.context().needs_update() - } - - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. - pub fn needs_update_any(&self, id: ScopeId) { - self.context().needs_update_any(id) - } - - /// Return any context of type T if it exists on this scope - pub fn has_context(&self) -> Option { - self.context().has_context() - } - - /// Try to retrieve a shared state with type `T` from any parent scope. - /// - /// Clones the state if it exists. - pub fn consume_context(&self) -> Option { - self.context().consume_context() - } - - /// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree. - /// - /// This is a "fundamental" operation and should only be called during initialization of a hook. - /// - /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead. - /// - /// # Example - /// - /// ```rust, ignore - /// struct SharedState(&'static str); - /// - /// static App: Component = |cx| { - /// cx.use_hook(|| cx.provide_context(SharedState("world"))); - /// render!(Child {}) - /// } - /// - /// static Child: Component = |cx| { - /// let state = cx.consume_state::(); - /// render!(div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_context(&self, value: T) -> T { - self.context().provide_context(value) - } - - /// Provide a context to the root and then consume it - /// - /// This is intended for "global" state management solutions that would rather be implicit for the entire app. - /// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization. - /// - /// Note that you should be checking if the context existed before trying to provide a new one. Providing a context - /// when a context already exists will swap the context out for the new one, which may not be what you want. - pub fn provide_root_context(&self, context: T) -> T { - self.context().provide_root_context(context) - } - - /// Pushes the future onto the poll queue to be polled after the component renders. - pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { - self.context().push_future(fut) - } - - /// Spawns the future but does not return the [`TaskId`] - pub fn spawn(&self, fut: impl Future + 'static) { - self.context().spawn(fut); - } - - /// 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. - pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { - self.context().spawn_forever(fut) - } - - /// 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) { - self.context().remove_future(id); - } - - /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator - pub fn text_node(&'src self, args: Arguments) -> DynamicNode { - DynamicNode::Text(VText { - value: args.to_string(), - id: Default::default(), - }) - } - - /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator - pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode + 'c) -> DynamicNode { - into.into_dyn_node() - } - - /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool - /// - /// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in - /// some renderers stay in sync with the VirtualDom's understanding of the world - pub fn attr( - &'src self, - name: &'static str, - value: impl IntoAttributeValue, - namespace: Option<&'static str>, - volatile: bool, - ) -> Attribute { - Attribute { - name, - namespace, - volatile, - mounted_element: Default::default(), - value: value.into_value(), - } - } - - /// Create a new [`DynamicNode::Component`] variant - /// - /// - /// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result`]. - /// - /// ```rust, ignore - /// // Without explicit props - /// fn(Scope) -> Element; - /// async fn(Scope<'_>) -> Element; - /// - /// // With explicit props - /// fn(Scope) -> Element; - /// async fn(Scope>) -> Element; - /// ``` - pub fn component

( - &self, - component: fn(P) -> Element, - props: P, - fn_name: &'static str, - ) -> DynamicNode - where - // The properties must be valid until the next bump frame - P: Properties, - { - let vcomp = VProps::new(component, P::memoize, props, fn_name); - - DynamicNode::Component(VComponent { - name: fn_name, - render_fn: component as *const (), - props: BoxedAnyProps::new(vcomp), - scope: Default::default(), - }) - } - - /// Create a new [`EventHandler`] from an [`FnMut`] - pub fn event_handler(&self, mut f: impl FnMut(T) + 'static) -> EventHandler { - let callback = RefCell::new(Some(Box::new(move |event: T| { - f(event); - }) as Box)); - EventHandler { - callback, - origin: self.context().id, - } - } - - /// Create a new [`AttributeValue`] with the listener variant from a callback - /// - /// The callback must be confined to the lifetime of the ScopeState - pub fn listener( - &self, - mut callback: impl FnMut(Event) + 'static, - ) -> AttributeValue { - AttributeValue::Listener(RefCell::new(Box::new(move |event: Event| { - if let Ok(data) = event.data.downcast::() { - callback(Event { - propagates: event.propagates, - data, - }); - } - }))) - } - - /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] - pub fn any_value(&'src self, value: T) -> AttributeValue { - AttributeValue::Any(Box::new(value)) - } - - /// Mark this component as suspended and then return None - pub fn suspend(&self) -> Option { - let cx = self.context(); - cx.suspend(); - None - } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5053bcae7..170261b0a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -276,10 +276,11 @@ impl VirtualDom { ); // Unlike react, we provide a default error boundary that just renders the error as a string - root.provide_context(Rc::new(ErrorBoundary::new_in_scope( - ScopeId::ROOT, - Arc::new(|_| {}), - ))); + root.context() + .provide_context(Rc::new(ErrorBoundary::new_in_scope( + ScopeId::ROOT, + Arc::new(|_| {}), + ))); // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(None); @@ -305,7 +306,7 @@ impl VirtualDom { /// /// This is useful for what is essentially dependency injection when building the app pub fn with_root_context(self, context: T) -> Self { - self.base_scope().provide_context(context); + self.base_scope().context().provide_context(context); self } @@ -314,7 +315,7 @@ impl VirtualDom { /// Whenever the Runtime "works", it will re-render this scope pub fn mark_dirty(&mut self, id: ScopeId) { if let Some(scope) = self.get_scope(id) { - let height = scope.height(); + let height = scope.context().height(); tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.context().name); self.dirty_scopes.insert(DirtyScope { height, id }); } @@ -403,11 +404,8 @@ impl VirtualDom { // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.into_iter().rev() { if let AttributeValue::Listener(listener) = listener { - let origin = path.scope; - self.runtime.scope_stack.borrow_mut().push(origin); self.runtime.rendering.set(false); - (listener.borrow_mut())(uievent.clone()); - self.runtime.scope_stack.borrow_mut().pop(); + listener.call(uievent.clone()); self.runtime.rendering.set(true); if !uievent.propagates.get() { @@ -432,11 +430,8 @@ impl VirtualDom { // Only call the listener if this is the exact target element. if attr.name.trim_start_matches("on") == name && target_path == this_path { if let AttributeValue::Listener(listener) = &attr.value { - let origin = path.scope; - self.runtime.scope_stack.borrow_mut().push(origin); self.runtime.rendering.set(false); - (listener.borrow_mut())(uievent.clone()); - self.runtime.scope_stack.borrow_mut().pop(); + listener.call(uievent.clone()); self.runtime.rendering.set(true); break;