diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 73c468fa3..fa6686a46 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -11,27 +11,26 @@ pub(crate) trait AnyProps { fn duplicate(&self) -> BoxedAnyProps; } -pub(crate) struct VProps { - pub render_fn: Component

, - pub memo: fn(&P, &P) -> bool, - pub props: P, - pub name: &'static str, +/// Create a new boxed props object. +pub fn new_any_props( + render_fn: Component

, + memo: fn(&P, &P) -> bool, + props: P, + name: &'static str, +) -> Box { + Box::new(VProps { + render_fn, + memo, + props, + name, + }) } -impl VProps

{ - pub(crate) fn new( - render_fn: Component

, - memo: fn(&P, &P) -> bool, - props: P, - name: &'static str, - ) -> Self { - Self { - render_fn, - memo, - props, - name, - } - } +struct VProps

{ + render_fn: Component

, + memo: fn(&P, &P) -> bool, + props: P, + name: &'static str, } impl AnyProps for VProps

{ diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index e948f4e37..333f4c5e6 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -27,6 +27,15 @@ pub struct Event { pub(crate) propagates: Rc>, } +impl Event { + pub(crate) fn new(data: Rc, bubbles: bool) -> Self { + Self { + data, + propagates: Rc::new(Cell::new(bubbles)), + } + } +} + impl Event { /// Map the event data to a new type /// diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 716f4b3a3..d358edb47 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -16,10 +16,10 @@ mod nodes; mod platform; mod properties; mod runtime; -mod scheduler; mod scope_arena; mod scope_context; mod scopes; +mod tasks; mod virtual_dom; pub(crate) mod innerlude { @@ -34,8 +34,8 @@ pub(crate) mod innerlude { pub use crate::platform::*; pub use crate::properties::*; pub use crate::runtime::{Runtime, RuntimeGuard}; - pub use crate::scheduler::*; pub use crate::scopes::*; + pub use crate::tasks::*; pub use crate::virtual_dom::*; /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 4de53955c..0972e534d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,5 +1,5 @@ use crate::{ - any_props::{BoxedAnyProps, VProps}, + any_props::{new_any_props, BoxedAnyProps}, innerlude::ScopeState, }; use crate::{arena::ElementId, Element, Event}; @@ -520,36 +520,22 @@ 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() -> Element; - /// async fn(Scope<'_>) -> Element; - /// - /// // With explicit props - /// fn(Props) -> Element; - /// async fn(Scope>) -> Element; - /// ``` pub fn new( component: impl ComponentFunction, props: P, fn_name: &'static str, ) -> Self where - // The properties must be valid until the next bump frame P: Properties + 'static, { let component = Rc::new(component); let render_fn = component.id(); let component = component.as_component(); - let vcomp = VProps::new(component,

::memoize, props, fn_name); + let props = new_any_props(component,

::memoize, props, fn_name); VComponent { name: fn_name, - props: Box::new(vcomp), + props, render_fn, } } diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index eda52e9a6..240e95300 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -69,9 +69,9 @@ pub struct Runtime { pub(crate) rendering: Cell, /// Tasks created with cx.spawn - pub tasks: RefCell>, + pub(crate) tasks: RefCell>, - pub sender: futures_channel::mpsc::UnboundedSender, + pub(crate) sender: futures_channel::mpsc::UnboundedSender, } impl Runtime { diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs deleted file mode 100644 index 03e419969..000000000 --- a/packages/core/src/scheduler/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::ScopeId; - -mod task; -mod wait; - -pub use task::*; - -/// The type of message that can be sent to the scheduler. -/// -/// These messages control how the scheduler will process updates to the UI. -#[derive(Debug)] -pub(crate) enum SchedulerMsg { - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// A task has woken and needs to be progressed - TaskNotified(Task), -} diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs deleted file mode 100644 index 77c2981ae..000000000 --- a/packages/core/src/scheduler/suspense.rs +++ /dev/null @@ -1,32 +0,0 @@ -use futures_util::task::ArcWake; - -use super::SchedulerMsg; -use crate::ElementId; -use crate::{innerlude::Mutations, Element, ScopeId}; -use std::future::Future; -use std::sync::Arc; -use std::task::Waker; -use std::{ - cell::{Cell, RefCell}, - collections::HashSet, -}; - -/// A boundary in the VirtualDom that captures all suspended components below it -pub struct SuspenseContext { - pub(crate) id: ScopeId, - pub(crate) waiting_on: RefCell>, -} - -impl SuspenseContext { - /// Create a new boundary for suspense - pub fn new(id: ScopeId) -> Self { - Self { - id, - waiting_on: Default::default(), - } - } - - pub fn mark_suspend(&self, id: ScopeId) { - self.waiting_on.borrow_mut().insert(id); - } -} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs deleted file mode 100644 index 2f208a525..000000000 --- a/packages/core/src/scheduler/wait.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::{runtime::RuntimeGuard, Task, VirtualDom}; -use std::task::Context; - -impl VirtualDom { - /// Handle notifications by tasks inside the scheduler - /// - /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the - /// queue - pub(crate) fn handle_task_wakeup(&mut self, id: Task) { - let _runtime = RuntimeGuard::new(self.runtime.clone()); - let mut tasks = self.runtime.tasks.borrow_mut(); - - let task = match tasks.get(id.0) { - Some(task) => task, - // The task was removed from the scheduler, so we can just ignore it - None => return, - }; - - let mut cx = Context::from_waker(&task.waker); - - // update the scope stack - self.runtime.scope_stack.borrow_mut().push(task.scope); - self.runtime.rendering.set(false); - self.runtime.current_task.set(Some(id)); - - // If the task completes... - if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { - // Remove it from the scope so we dont try to double drop it when the scope dropes - let scope = &self.get_scope(task.scope).unwrap(); - scope.context().spawned_tasks.borrow_mut().remove(&id); - - // Remove it from the scheduler - tasks.try_remove(id.0); - } - - // Remove the scope from the stack - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); - self.runtime.current_task.set(None); - } -} diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/tasks.rs similarity index 64% rename from packages/core/src/scheduler/task.rs rename to packages/core/src/tasks.rs index d8e647fe9..950f9fc19 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/tasks.rs @@ -1,8 +1,6 @@ -use futures_util::task::ArcWake; - -use super::SchedulerMsg; use crate::innerlude::{remove_future, spawn, Runtime}; use crate::ScopeId; +use futures_util::task::ArcWake; use std::cell::RefCell; use std::future::Future; use std::pin::Pin; @@ -11,8 +9,7 @@ use std::task::Waker; /// A task's unique identifier. /// -/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused -/// once a Task has been completed. +/// `Task` is a unique identifier for a task that has been spawned onto the runtime. It can be used to cancel the task #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct Task(pub(crate) usize); @@ -39,14 +36,6 @@ impl Task { } } -/// the task itself is the waker -pub(crate) struct LocalTask { - pub scope: ScopeId, - pub parent: Option, - pub task: RefCell + 'static>>>, - pub waker: Waker, -} - impl Runtime { /// Start a new future on the same thread as the rest of the VirtualDom. /// @@ -86,6 +75,43 @@ impl Runtime { task_id } + pub(crate) fn handle_task_wakeup(&self, id: Task) { + let mut tasks = self.tasks.borrow_mut(); + + let task = match tasks.get(id.0) { + Some(task) => task, + // The task was removed from the scheduler, so we can just ignore it + None => return, + }; + + use std::task::Context; + + let mut cx = Context::from_waker(&task.waker); + + // update the scope stack + self.scope_stack.borrow_mut().push(task.scope); + self.rendering.set(false); + self.current_task.set(Some(id)); + + // If the task completes... + if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + // Remove it from the scope so we dont try to double drop it when the scope dropes + self.get_context(task.scope) + .unwrap() + .spawned_tasks + .borrow_mut() + .remove(&id); + + // Remove it from the scheduler + tasks.try_remove(id.0); + } + + // Remove the scope from the stack + self.scope_stack.borrow_mut().pop(); + self.rendering.set(true); + self.current_task.set(None); + } + /// Drop the future with the given TaskId /// /// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you @@ -97,9 +123,34 @@ impl Runtime { pub fn current_task(&self) -> Option { self.current_task.get() } + + /// Get the parent task of the given task, if it exists + pub fn parent_task(&self, task: Task) -> Option { + self.tasks.borrow().get(task.0)?.parent + } } -pub struct LocalTaskHandle { +/// the task itself is the waker +pub(crate) struct LocalTask { + scope: ScopeId, + parent: Option, + task: RefCell + 'static>>>, + waker: Waker, +} + +/// The type of message that can be sent to the scheduler. +/// +/// These messages control how the scheduler will process updates to the UI. +#[derive(Debug)] +pub(crate) enum SchedulerMsg { + /// Immediate updates from Components that mark them as dirty + Immediate(ScopeId), + + /// A task has woken and needs to be progressed + TaskNotified(Task), +} + +struct LocalTaskHandle { id: Task, tx: futures_channel::mpsc::UnboundedSender, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3f1aeb94a..8780586be 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -3,7 +3,7 @@ //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. use crate::{ - any_props::VProps, + any_props::new_any_props, arena::ElementId, innerlude::{ DirtyScope, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState, VNodeMount, @@ -14,12 +14,12 @@ use crate::{ properties::ComponentFunction, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, - AttributeValue, BoxedContext, Element, Event, Mutations, + AttributeValue, BoxedContext, Element, Event, Mutations, Task, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -287,12 +287,7 @@ impl VirtualDom { }; let root = dom.new_scope( - Box::new(VProps::new( - Rc::new(root).as_component(), - |_, _| true, - root_props, - "root", - )), + new_any_props(Rc::new(root).as_component(), |_, _| true, root_props, "app"), "app", ); @@ -363,7 +358,6 @@ impl VirtualDom { /// It is up to the listeners themselves to mark nodes as dirty. /// /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline" - pub fn handle_event( &mut self, name: &str, @@ -394,87 +388,86 @@ impl VirtualDom { | | | <-- no, broke early | <-- no, broke early */ - let parent_path = match self.elements.get(element.0) { - Some(Some(el)) => *el, - _ => return, + let Some(Some(parent_path)) = self.elements.get(element.0).copied() else { + return; }; - let mut parent_node = Some(parent_path); // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required - let uievent = Event { - propagates: Rc::new(Cell::new(bubbles)), - data, - }; + let uievent = Event::new(data, bubbles); + + // Use the simple non-bubbling algorithm if the event doesn't bubble + if !bubbles { + return self.handle_non_bubbling_event(parent_path, name, uievent); + } + + let mut parent_node = Some(parent_path); // If the event bubbles, we traverse through the tree until we find the target element. - if bubbles { - // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. - while let Some(path) = parent_node { - let mut listeners = vec![]; + // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. + while let Some(path) = parent_node { + let mut listeners = vec![]; - let el_ref = &self.mounts[path.mount.0].node; - let node_template = el_ref.template.get(); - let target_path = path.path; + let el_ref = &self.mounts[path.mount.0].node; + let node_template = el_ref.template.get(); + let target_path = path.path; - for (idx, attrs) in el_ref.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + for (idx, attrs) in el_ref.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; - for attr in attrs.iter() { - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.trim_start_matches("on") == name - && target_path.is_decendant(&this_path) - { - listeners.push(&attr.value); + for attr in attrs.iter() { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if attr.name.trim_start_matches("on") == name + && target_path.is_decendant(&this_path) + { + listeners.push(&attr.value); - // 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 target_path == this_path { - break; - } + // 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 target_path == this_path { + break; } } } + } - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // 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 { + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // 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 { + self.runtime.rendering.set(false); + listener.call(uievent.clone()); + self.runtime.rendering.set(true); + + if !uievent.propagates.get() { + return; + } + } + } + + let mount = el_ref.mount.get().as_usize(); + parent_node = mount.and_then(|id| self.mounts.get(id).and_then(|el| el.parent)); + } + } + + /// Call an event listener in the simplest way possible without bubbling upwards + fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event) { + let el_ref = &self.mounts[node.mount.0].node; + let node_template = el_ref.template.get(); + let target_path = node.path; + + for (idx, attr) in el_ref.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; + + for attr in attr.iter() { + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // 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 { self.runtime.rendering.set(false); listener.call(uievent.clone()); self.runtime.rendering.set(true); - - if !uievent.propagates.get() { - return; - } - } - } - - let mount = el_ref.mount.get().as_usize(); - parent_node = mount.and_then(|id| self.mounts.get(id).and_then(|el| el.parent)); - } - } else { - // Otherwise, we just call the listener on the target element - if let Some(path) = parent_node { - let el_ref = &self.mounts[path.mount.0].node; - let node_template = el_ref.template.get(); - let target_path = path.path; - - for (idx, attr) in el_ref.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; - - for attr in attr.iter() { - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - // 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 { - self.runtime.rendering.set(false); - listener.call(uievent.clone()); - self.runtime.rendering.set(true); - - break; - } - } + break; } } } @@ -501,27 +494,26 @@ impl VirtualDom { let mut some_msg = None; loop { - match some_msg.take() { - // If a bunch of messages are ready in a sequence, try to pop them off synchronously - Some(msg) => match msg { + // If a bunch of messages are ready in a sequence, try to pop them off synchronously + if let Some(msg) = some_msg.take() { + match msg { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), - }, + } + continue; + } - // If they're not ready, then we should wait for them to be ready - None => { - match self.rx.try_next() { - Ok(Some(val)) => some_msg = Some(val), - Ok(None) => return, - Err(_) => { - // If we have any dirty scopes, or finished fiber trees then we should exit - if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() { - return; - } - - some_msg = self.rx.next().await - } + // If they're not ready, then we should wait for them to be ready + match self.rx.try_next() { + Ok(Some(val)) => some_msg = Some(val), + Ok(None) => return, + Err(_) => { + // If we have any dirty scopes, or finished fiber trees then we should exit + if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() { + return; } + + some_msg = self.rx.next().await } } } @@ -537,6 +529,15 @@ impl VirtualDom { } } + /// Handle notifications by tasks inside the scheduler + /// + /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the + /// queue + fn handle_task_wakeup(&mut self, id: Task) { + let _runtime = RuntimeGuard::new(self.runtime.clone()); + self.runtime.handle_task_wakeup(id); + } + /// Replace a template at runtime. This will re-render all components that use this template. /// This is the primitive that enables hot-reloading. ///