diff --git a/Cargo.toml b/Cargo.toml index 4c420ad3e..5b55f4628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", + "packages/generational-box", "packages/signals", "packages/hot-reload", "packages/fullstack", @@ -76,6 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" } dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" } rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" } dioxus-signals = { path = "packages/signals" } +generational-box = { path = "packages/generational-box" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" } diff --git a/examples/clock.rs b/examples/clock.rs index 505a5e87c..f9c1892d9 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,17 +1,11 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. - use dioxus::prelude::*; -use dioxus_signals::{use_init_signal_rt, use_signal}; +use dioxus_signals::use_signal; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - use_init_signal_rt(cx); - let mut count = use_signal(cx, || 0); use_future!(cx, || async move { diff --git a/examples/signals.rs b/examples/signals.rs index 725577206..bc627fa8f 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } - if count() > 5 { + if count.value() > 5 { rsx!{ h2 { "High five!" } } } }) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index b6254136d..c8f7ce3c9 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -19,6 +19,9 @@ pub(crate) struct ElementRef { // The actual template pub template: Option>>, + + // The scope the element belongs to + pub scope: ScopeId, } #[derive(Clone, Copy, Debug)] @@ -32,6 +35,7 @@ impl ElementRef { Self { template: None, path: ElementPath::Root(0), + scope: ScopeId(0), } } } @@ -56,11 +60,13 @@ impl VirtualDom { fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); + let scope = self.runtime.current_scope_id().unwrap_or(ScopeId(0)); entry.insert(ElementRef { // We know this is non-null because it comes from a reference template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), path, + scope, }); ElementId(id) } @@ -91,7 +97,7 @@ impl VirtualDom { // Note: This will not remove any ids from the arena pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) { self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[id.0].height, + height: self.scopes[id.0].height(), id, }); @@ -110,10 +116,13 @@ impl VirtualDom { // Drop all the hooks once the children are dropped // this means we'll drop hooks bottom-up scope.hooks.get_mut().clear(); + { + let context = scope.context(); - // Drop all the futures once the hooks are dropped - for task_id in scope.spawned_tasks.borrow_mut().drain() { - scope.tasks.remove(task_id); + // Drop all the futures once the hooks are dropped + for task_id in context.spawned_tasks.borrow_mut().drain() { + context.tasks.remove(task_id); + } } self.scopes.remove(id.0); diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 6159c882b..7fbc8a9c8 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -66,10 +66,10 @@ impl<'b> VirtualDom { /// /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created. pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize { - self.scope_stack.push(scope); - let out = self.create(template); - self.scope_stack.pop(); - out + self.runtime.scope_stack.borrow_mut().push(scope); + let nodes = self.create(template); + self.runtime.scope_stack.borrow_mut().pop(); + nodes } /// Create this template and write its mutations @@ -522,7 +522,7 @@ impl<'b> VirtualDom { .take() .map(|props| { let unbounded_props: Box = unsafe { std::mem::transmute(props) }; - self.new_scope(unbounded_props, component.name).id + self.new_scope(unbounded_props, component.name).context().id }) .unwrap_or_else(|| component.scope.get().unwrap()) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 7865224aa..270c81730 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -15,9 +15,7 @@ use DynamicNode::*; impl<'b> VirtualDom { pub(super) fn diff_scope(&mut self, scope: ScopeId) { - let scope_state = &mut self.scopes[scope.0]; - - self.scope_stack.push(scope); + let scope_state = &mut self.get_scope(scope).unwrap(); unsafe { // Load the old and new bump arenas let old = scope_state @@ -47,7 +45,6 @@ impl<'b> VirtualDom { (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), }; } - self.scope_stack.pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { @@ -210,7 +207,7 @@ impl<'b> VirtualDom { self.diff_scope(scope_id); self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[scope_id.0].height, + height: self.runtime.get_context(scope_id).unwrap().height, id: scope_id, }); } @@ -714,7 +711,12 @@ impl<'b> VirtualDom { Component(comp) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + match unsafe { + self.get_scope(scope) + .unwrap() + .root_node() + .extend_lifetime_ref() + } { RenderReturn::Ready(node) => self.push_all_real_nodes(node), RenderReturn::Aborted(_node) => todo!(), } @@ -915,7 +917,12 @@ impl<'b> VirtualDom { .expect("VComponents to always have a scope"); // Remove the component from the dom - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + match unsafe { + self.get_scope(scope) + .unwrap() + .root_node() + .extend_lifetime_ref() + } { RenderReturn::Ready(t) => self.remove_node(t, gen_muts), RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts), }; @@ -936,7 +943,12 @@ impl<'b> VirtualDom { Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + match unsafe { + self.get_scope(scope) + .unwrap() + .root_node() + .extend_lifetime_ref() + } { RenderReturn::Ready(t) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } @@ -952,7 +964,12 @@ impl<'b> VirtualDom { Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + match unsafe { + self.get_scope(scope) + .unwrap() + .root_node() + .extend_lifetime_ref() + } { RenderReturn::Ready(t) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index ce958db40..b85e0394d 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,3 +1,4 @@ +use crate::{runtime::with_runtime, ScopeId}; use std::{ cell::{Cell, RefCell}, rc::Rc, @@ -135,12 +136,14 @@ impl std::fmt::Debug for Event { /// /// ``` pub struct EventHandler<'bump, T = ()> { + pub(crate) origin: ScopeId, pub(super) callback: RefCell>>, } impl Default for EventHandler<'_, T> { fn default() -> Self { Self { + origin: ScopeId(0), callback: Default::default(), } } @@ -154,7 +157,13 @@ impl EventHandler<'_, T> { /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { + with_runtime(|rt| { + rt.scope_stack.borrow_mut().push(self.origin); + }); callback(event); + with_runtime(|rt| { + rt.scope_stack.borrow_mut().pop(); + }); } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 565ae12be..62e142f60 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -14,8 +14,10 @@ mod lazynodes; mod mutations; mod nodes; mod properties; +mod runtime; mod scheduler; mod scope_arena; +mod scope_context; mod scopes; mod virtual_dom; @@ -31,6 +33,7 @@ pub(crate) mod innerlude { pub use crate::nodes::*; pub use crate::properties::*; pub use crate::scheduler::*; + pub use crate::scope_context::*; pub use crate::scopes::*; pub use crate::virtual_dom::*; @@ -70,10 +73,11 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, - Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, - Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, - TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, + CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, + LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, + TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, + VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -81,9 +85,12 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment, - IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, - Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, + provide_context, provide_context_to_scope, provide_root_context, push_future, + remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, + Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes, + Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, + TemplateNode, Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs new file mode 100644 index 000000000..b2b535830 --- /dev/null +++ b/packages/core/src/runtime.rs @@ -0,0 +1,108 @@ +use std::cell::{Cell, Ref, RefCell}; + +use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId}; +use std::rc::Rc; + +thread_local! { + static RUNTIMES: RefCell>> = RefCell::new(vec![]); +} + +/// Pushes a new scope onto the stack +pub(crate) fn push_runtime(runtime: Rc) { + RUNTIMES.with(|stack| stack.borrow_mut().push(runtime)); +} + +/// Pops a scope off the stack +pub(crate) fn pop_runtime() { + RUNTIMES.with(|stack| stack.borrow_mut().pop()); +} + +/// Runs a function with the current runtime +pub(crate) fn with_runtime(f: F) -> Option +where + F: FnOnce(&Runtime) -> R, +{ + RUNTIMES.with(|stack| { + let stack = stack.borrow(); + stack.last().map(|r| f(r)) + }) +} + +/// Runs a function with the current scope +pub(crate) fn with_current_scope(f: F) -> Option +where + F: FnOnce(&ScopeContext) -> R, +{ + with_runtime(|runtime| { + runtime + .current_scope_id() + .and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc))) + }) + .flatten() +} + +pub(crate) struct Runtime { + pub(crate) scope_contexts: RefCell>>, + pub(crate) scheduler: Rc, + + // We use this to track the current scope + pub(crate) scope_stack: RefCell>, + pub(crate) rendering: Cell, +} + +impl Runtime { + pub(crate) fn new(scheduler: Rc) -> Rc { + Rc::new(Self { + scheduler, + + scope_contexts: Default::default(), + + scope_stack: Default::default(), + + rendering: Cell::new(true), + }) + } + + /// Create a scope context. This slab is synchronized with the scope slab. + pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) { + let mut contexts = self.scope_contexts.borrow_mut(); + if contexts.len() <= id.0 { + contexts.resize_with(id.0 + 1, Default::default); + } + contexts[id.0] = Some(context); + } + + pub(crate) fn remove_context(&self, id: ScopeId) { + self.scope_contexts.borrow_mut()[id.0] = None; + } + + /// Get the current scope id + pub fn current_scope_id(&self) -> Option { + self.scope_stack.borrow().last().copied() + } + + /// Get the context for any scope given its ID + /// + /// This is useful for inserting or removing contexts from a scope, or rendering out its root node + pub fn get_context(&self, id: ScopeId) -> Option> { + Ref::filter_map(self.scope_contexts.borrow(), |contexts| { + contexts.get(id.0).and_then(|f| f.as_ref()) + }) + .ok() + } +} + +pub(crate) struct RuntimeGuard(Rc); + +impl RuntimeGuard { + pub(crate) fn new(runtime: Rc) -> Self { + push_runtime(runtime.clone()); + Self(runtime) + } +} + +impl Drop for RuntimeGuard { + fn drop(&mut self) { + pop_runtime(); + } +} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index b475c6751..625873293 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,4 +1,4 @@ -use crate::{TaskId, VirtualDom}; +use crate::{runtime::RuntimeGuard, TaskId, VirtualDom}; use std::task::Context; impl VirtualDom { @@ -7,7 +7,8 @@ impl VirtualDom { /// 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: TaskId) { - let mut tasks = self.scheduler.tasks.borrow_mut(); + let _runtime = RuntimeGuard::new(self.runtime.clone()); + let mut tasks = self.runtime.scheduler.tasks.borrow_mut(); let task = match tasks.get(id.0) { Some(task) => task, @@ -17,14 +18,22 @@ impl VirtualDom { 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); + // 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.scopes[task.scope.0]; - scope.spawned_tasks.borrow_mut().remove(&id); + 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); } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index b298a8996..138431740 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -3,6 +3,7 @@ use crate::{ bump_frame::BumpFrame, innerlude::DirtyScope, nodes::RenderReturn, + scope_context::ScopeContext, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -13,48 +14,49 @@ impl VirtualDom { props: Box>, name: &'static str, ) -> &ScopeState { - let parent = self.acquire_current_scope_raw(); + let parent_id = self.runtime.current_scope_id(); + let height = parent_id + .and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1)) + .unwrap_or(0); let entry = self.scopes.vacant_entry(); - let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) }; let id = ScopeId(entry.key()); - entry.insert(Box::new(ScopeState { - parent, - id, - height, - name, + let scope = entry.insert(Box::new(ScopeState { + runtime: self.runtime.clone(), + context_id: id, + props: Some(props), - tasks: self.scheduler.clone(), + node_arena_1: BumpFrame::new(0), node_arena_2: BumpFrame::new(0), - spawned_tasks: Default::default(), - suspended: Default::default(), + render_cnt: Default::default(), hooks: Default::default(), hook_idx: Default::default(), - shared_contexts: Default::default(), + borrowed_props: Default::default(), attributes_to_drop: Default::default(), - })) - } + })); - fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> { - let id = self.scope_stack.last().copied()?; - let scope = self.scopes.get(id.0)?; - Some(scope.as_ref()) + let context = + ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone()); + self.runtime.create_context_at(id, context); + + scope } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + self.runtime.scope_stack.borrow_mut().push(scope_id); // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners self.ensure_drop_safety(scope_id); let new_nodes = unsafe { - self.scopes[scope_id.0].previous_frame().bump_mut().reset(); - let scope = &self.scopes[scope_id.0]; - scope.suspended.set(false); + scope.previous_frame().bump_mut().reset(); + + scope.context().suspended.set(false); scope.hook_idx.set(0); @@ -77,21 +79,26 @@ impl VirtualDom { // And move the render generation forward by one scope.render_cnt.set(scope.render_cnt.get() + 1); + let context = scope.context(); // remove this scope from dirty scopes self.dirty_scopes.remove(&DirtyScope { - height: scope.height, - id: scope.id, + height: context.height, + id: context.id, }); - if scope.suspended.get() { + if context.suspended.get() { if matches!(allocated, RenderReturn::Aborted(_)) { - self.suspended_scopes.insert(scope.id); + self.suspended_scopes.insert(context.id); } } else if !self.suspended_scopes.is_empty() { - _ = self.suspended_scopes.remove(&scope.id); + _ = self.suspended_scopes.remove(&context.id); } // rebind the lifetime now that its stored internally - unsafe { allocated.extend_lifetime_ref() } + let result = unsafe { allocated.extend_lifetime_ref() }; + + self.runtime.scope_stack.borrow_mut().pop(); + + result } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs new file mode 100644 index 000000000..14366eefb --- /dev/null +++ b/packages/core/src/scope_context.rs @@ -0,0 +1,335 @@ +use crate::{ + innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, + runtime::{with_current_scope, with_runtime}, + Element, ScopeId, TaskId, +}; +use rustc_hash::FxHashSet; +use std::{ + any::Any, + cell::{Cell, RefCell}, + fmt::Debug, + future::Future, + rc::Rc, + sync::Arc, +}; + +/// A component's state separate from its props. +/// +/// This struct exists to provide a common interface for all scopes without relying on generics. +pub(crate) struct ScopeContext { + pub(crate) name: &'static str, + + pub(crate) id: ScopeId, + pub(crate) parent_id: Option, + + pub(crate) height: u32, + pub(crate) suspended: Cell, + + pub(crate) shared_contexts: RefCell>>, + + pub(crate) tasks: Rc, + pub(crate) spawned_tasks: RefCell>, +} + +impl ScopeContext { + pub(crate) fn new( + name: &'static str, + id: ScopeId, + parent_id: Option, + height: u32, + tasks: Rc, + ) -> Self { + Self { + name, + id, + parent_id, + height, + suspended: Cell::new(false), + shared_contexts: RefCell::new(vec![]), + tasks, + spawned_tasks: RefCell::new(FxHashSet::default()), + } + } + + pub fn parent_id(&self) -> Option { + self.parent_id + } + + pub fn scope_id(&self) -> ScopeId { + self.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 { + let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); + Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) + } + + /// 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 { + let chan = self.tasks.sender.clone(); + Arc::new(move |id| { + chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap(); + }) + } + + /// Mark this scope as dirty, and schedule a render for it. + pub fn needs_update(&self) { + self.needs_update_any(self.scope_id()); + } + + /// 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.tasks + .sender + .unbounded_send(SchedulerMsg::Immediate(id)) + .expect("Scheduler to exist if scope exists"); + } + + /// Return any context of type T if it exists on this scope + pub fn has_context(&self) -> Option { + self.shared_contexts + .borrow() + .iter() + .find_map(|any| any.downcast_ref::()) + .cloned() + } + + /// 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 { + if let Some(this_ctx) = self.has_context() { + return Some(this_ctx); + } + + let mut search_parent = self.parent_id; + with_runtime(|runtime| { + while let Some(parent_id) = search_parent { + let parent = runtime.get_context(parent_id).unwrap(); + if let Some(shared) = parent + .shared_contexts + .borrow() + .iter() + .find_map(|any| any.downcast_ref::()) + { + return Some(shared.clone()); + } + search_parent = parent.parent_id; + } + None + }) + .flatten() + } + + /// 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 { + let mut contexts = self.shared_contexts.borrow_mut(); + + // If the context exists, swap it out for the new value + for ctx in contexts.iter_mut() { + // Swap the ptr directly + if let Some(ctx) = ctx.downcast_mut::() { + std::mem::swap(ctx, &mut value.clone()); + return value; + } + } + + // Else, just push it + contexts.push(Box::new(value.clone())); + + 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 { + with_runtime(|runtime| { + runtime + .get_context(ScopeId(0)) + .unwrap() + .provide_context(context) + }) + .expect("Runtime to exist") + } + + /// Pushes the future onto the poll queue to be polled after the component renders. + pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { + let id = self.tasks.spawn(self.id, fut); + self.spawned_tasks.borrow_mut().insert(id); + id + } + + /// Spawns the future but does not return the [`TaskId`] + pub fn spawn(&self, fut: impl Future + 'static) { + self.push_future(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 { + // The root scope will never be unmounted so we can just add the task at the top of the app + let id = self.tasks.spawn(ScopeId(0), fut); + + // wake up the scheduler if it is sleeping + self.tasks + .sender + .unbounded_send(SchedulerMsg::TaskNotified(id)) + .expect("Scheduler should exist"); + + self.spawned_tasks.borrow_mut().insert(id); + + id + } + + /// 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.tasks.remove(id); + } + + /// Inject an error into the nearest error boundary and quit rendering + /// + /// The error doesn't need to implement Error or any specific traits since the boundary + /// itself will downcast the error into a trait object. + pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { + if let Some(cx) = self.consume_context::>() { + cx.insert_error(self.scope_id(), Box::new(error)); + } + + // Always return none during a throw + None + } + + /// Mark this component as suspended and then return None + pub fn suspend(&self) -> Option { + self.suspended.set(true); + None + } +} + +/// Schedule an update for any component given its [`ScopeId`]. +/// +/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`crate::scopes::ScopeState::scope_id`] method. +/// +/// This method should be used when you want to schedule an update for a component +pub fn schedule_update_any() -> Option> { + with_current_scope(|cx| cx.schedule_update_any()) +} + +/// Get the current scope id +pub fn current_scope_id() -> Option { + with_runtime(|rt| rt.current_scope_id()).flatten() +} + +#[doc(hidden)] +/// Check if the virtual dom is currently inside of the body of a component +pub fn vdom_is_rendering() -> bool { + with_runtime(|rt| rt.rendering.get()).unwrap_or_default() +} + +/// Consume context from the current scope +pub fn consume_context() -> Option { + with_current_scope(|cx| cx.consume_context::()).flatten() +} + +/// Consume context from the current scope +pub fn consume_context_from_scope(scope_id: ScopeId) -> Option { + with_runtime(|rt| { + rt.get_context(scope_id) + .and_then(|cx| cx.consume_context::()) + }) + .flatten() +} + +/// Check if the current scope has a context +pub fn has_context() -> Option { + with_current_scope(|cx| cx.has_context::()).flatten() +} + +/// Provide context to the current scope +pub fn provide_context(value: T) -> Option { + with_current_scope(|cx| cx.provide_context(value)) +} + +/// Provide context to the the given scope +pub fn provide_context_to_scope(scope_id: ScopeId, value: T) -> Option { + with_runtime(|rt| rt.get_context(scope_id).map(|cx| cx.provide_context(value))).flatten() +} + +/// Provide a context to the root scope +pub fn provide_root_context(value: T) -> Option { + with_current_scope(|cx| cx.provide_root_context(value)) +} + +/// Suspends the current component +pub fn suspend() -> Option> { + with_current_scope(|cx| { + cx.suspend(); + }); + None +} + +/// Throw an error into the nearest error boundary +pub fn throw(error: impl Debug + 'static) -> Option<()> { + with_current_scope(|cx| cx.throw(error)).flatten() +} + +/// Pushes the future onto the poll queue to be polled after the component renders. +pub fn push_future(fut: impl Future + 'static) -> Option { + with_current_scope(|cx| cx.push_future(fut)) +} + +/// Spawns the future but does not return the [`TaskId`] +pub fn spawn(fut: impl Future + 'static) { + with_current_scope(|cx| cx.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(fut: impl Future + 'static) -> Option { + with_current_scope(|cx| cx.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(id: TaskId) { + with_current_scope(|cx| cx.remove_future(id)); +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 4a21fcb78..5ada89069 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -2,17 +2,18 @@ use crate::{ any_props::AnyProps, any_props::VProps, bump_frame::BumpFrame, + innerlude::ErrorBoundary, innerlude::{DynamicNode, EventHandler, VComponent, VText}, - innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, + runtime::Runtime, + scope_context::ScopeContext, AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; -use rustc_hash::FxHashSet; use std::{ - any::{Any, TypeId}, - cell::{Cell, RefCell, UnsafeCell}, + any::Any, + cell::{Cell, Ref, RefCell, UnsafeCell}, fmt::{Arguments, Debug}, future::Future, rc::Rc, @@ -66,33 +67,34 @@ pub struct ScopeId(pub usize); /// /// This struct exists to provide a common interface for all scopes without relying on generics. pub struct ScopeState { + pub(crate) runtime: Rc, + pub(crate) context_id: ScopeId, + pub(crate) render_cnt: Cell, - pub(crate) name: &'static str, pub(crate) node_arena_1: BumpFrame, pub(crate) node_arena_2: BumpFrame, - pub(crate) parent: Option<*const ScopeState>, - pub(crate) id: ScopeId, - - pub(crate) height: u32, - pub(crate) suspended: Cell, - pub(crate) hooks: RefCell>>>, pub(crate) hook_idx: Cell, - pub(crate) shared_contexts: RefCell)>>, - - pub(crate) tasks: Rc, - pub(crate) spawned_tasks: RefCell>, - pub(crate) borrowed_props: RefCell>>, pub(crate) attributes_to_drop: RefCell>>, pub(crate) props: Option>>, } +impl Drop for ScopeState { + fn drop(&mut self) { + self.runtime.remove_context(self.context_id); + } +} + impl<'src> ScopeState { + pub(crate) fn context(&self) -> Ref<'_, ScopeContext> { + self.runtime.get_context(self.context_id).unwrap() + } + pub(crate) fn current_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 0 => &self.node_arena_1, @@ -111,7 +113,7 @@ impl<'src> ScopeState { /// Get the name of this component pub fn name(&self) -> &str { - self.name + self.context().name } /// Get the current render since the inception of this component @@ -174,7 +176,7 @@ impl<'src> ScopeState { /// assert_eq!(base.height(), 0); /// ``` pub fn height(&self) -> u32 { - self.height + self.context().height } /// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`]. @@ -195,7 +197,7 @@ impl<'src> ScopeState { /// ``` pub fn parent(&self) -> Option { // safety: the pointer to our parent is *always* valid thanks to the bump arena - self.parent.map(|p| unsafe { &*p }.id) + self.context().parent_id() } /// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`]. @@ -212,15 +214,14 @@ impl<'src> ScopeState { /// assert_eq!(base.scope_id(), 0); /// ``` pub fn scope_id(&self) -> ScopeId { - self.id + 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 { - let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); - Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) + self.context().schedule_update() } /// Schedule an update for any component given its [`ScopeId`]. @@ -229,61 +230,31 @@ impl<'src> ScopeState { /// /// This method should be used when you want to schedule an update for a component pub fn schedule_update_any(&self) -> Arc { - let chan = self.tasks.sender.clone(); - Arc::new(move |id| { - chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap(); - }) + self.context().schedule_update_any() } /// Mark this scope as dirty, and schedule a render for it. pub fn needs_update(&self) { - self.needs_update_any(self.scope_id()); + 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.tasks - .sender - .unbounded_send(SchedulerMsg::Immediate(id)) - .expect("Scheduler to exist if scope exists"); + 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.shared_contexts - .borrow() - .iter() - .find(|(k, _)| *k == TypeId::of::()) - .map(|(_, v)| v)? - .downcast_ref::() - .cloned() + 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 { - 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() - .iter() - .find(|(k, _)| *k == TypeId::of::()) - { - return shared.1.downcast_ref::().cloned(); - } - search_parent = parent.parent; - } - None + 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. @@ -308,21 +279,7 @@ impl<'src> ScopeState { /// } /// ``` pub fn provide_context(&self, value: T) -> T { - let mut contexts = self.shared_contexts.borrow_mut(); - - // If the context exists, swap it out for the new value - for ctx in contexts.iter_mut() { - // Swap the ptr directly - if let Some(ctx) = ctx.1.downcast_mut::() { - std::mem::swap(ctx, &mut value.clone()); - return value; - } - } - - // Else, just push it - contexts.push((TypeId::of::(), Box::new(value.clone()))); - - value + self.context().provide_context(value) } /// Provide a context to the root and then consume it @@ -333,52 +290,31 @@ impl<'src> ScopeState { /// 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 { - let mut parent = self; - - // Walk upwards until there is no more parent - and tada we have the root - while let Some(next_parent) = parent.parent { - parent = unsafe { &*next_parent }; - debug_assert_eq!(parent.scope_id(), ScopeId(0)); - } - - parent.provide_context(context) + 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 { - let id = self.tasks.spawn(self.id, fut); - self.spawned_tasks.borrow_mut().insert(id); - id + self.context().push_future(fut) } /// Spawns the future but does not return the [`TaskId`] pub fn spawn(&self, fut: impl Future + 'static) { - self.push_future(fut); + 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 { - // The root scope will never be unmounted so we can just add the task at the top of the app - let id = self.tasks.spawn(ScopeId(0), fut); - - // wake up the scheduler if it is sleeping - self.tasks - .sender - .unbounded_send(SchedulerMsg::TaskNotified(id)) - .expect("Scheduler should exist"); - - self.spawned_tasks.borrow_mut().insert(id); - - id + 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.tasks.remove(id); + self.context().remove_future(id); } /// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator. @@ -511,7 +447,10 @@ impl<'src> ScopeState { let handler: &mut dyn FnMut(T) = self.bump().alloc(f); let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; let callback = RefCell::new(Some(caller)); - EventHandler { callback } + EventHandler { + callback, + origin: self.context().id, + } } /// Create a new [`AttributeValue`] with the listener variant from a callback @@ -565,7 +504,8 @@ impl<'src> ScopeState { /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { - self.suspended.set(true); + let cx = self.context(); + cx.suspend(); None } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index dce5415f6..a1def71d2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,6 +9,7 @@ use crate::{ mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, + runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, AttributeValue, Element, Event, Scope, }; @@ -174,24 +175,24 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// } /// ``` pub struct VirtualDom { + pub(crate) scopes: Slab>, + + pub(crate) dirty_scopes: BTreeSet, + // Maps a template path to a map of byteindexes to templates pub(crate) templates: FxHashMap>>, - pub(crate) scopes: Slab>, - pub(crate) dirty_scopes: BTreeSet, - pub(crate) scheduler: Rc, // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template pub(crate) elements: Slab, - // While diffing we need some sort of way of breaking off a stream of suspended mutations. - pub(crate) scope_stack: Vec, + pub(crate) mutations: Mutations<'static>, + + pub(crate) runtime: Rc, // Currently suspended scopes pub(crate) suspended_scopes: FxHashSet, pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, - - pub(crate) mutations: Mutations<'static>, } impl VirtualDom { @@ -251,16 +252,16 @@ impl VirtualDom { /// ``` pub fn new_with_props(root: fn(Scope

) -> Element, root_props: P) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); + let scheduler = Scheduler::new(tx); let mut dom = Self { rx, - scheduler: Scheduler::new(tx), - templates: Default::default(), + runtime: Runtime::new(scheduler), scopes: Default::default(), + dirty_scopes: Default::default(), + templates: Default::default(), elements: Default::default(), - scope_stack: Vec::new(), - dirty_scopes: BTreeSet::new(), - suspended_scopes: FxHashSet::default(), mutations: Mutations::default(), + suspended_scopes: Default::default(), }; let root = dom.new_scope( @@ -281,7 +282,7 @@ impl VirtualDom { /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get(id.0).map(|f| f.as_ref()) + self.scopes.get(id.0).map(|s| &**s) } /// Get the single scope at the top of the VirtualDom tree that will always be around @@ -301,10 +302,10 @@ impl VirtualDom { /// Manually mark a scope as requiring a re-render /// - /// Whenever the VirtualDom "works", it will re-render this scope + /// 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.height(); self.dirty_scopes.insert(DirtyScope { height, id }); } } @@ -325,6 +326,8 @@ impl VirtualDom { element: ElementId, bubbles: bool, ) { + let _runtime = RuntimeGuard::new(self.runtime.clone()); + /* ------------------------ The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when @@ -387,9 +390,14 @@ impl VirtualDom { // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.drain(..).rev() { if let AttributeValue::Listener(listener) = listener { + let origin = el_ref.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + self.runtime.rendering.set(false); if let Some(cb) = listener.borrow_mut().as_deref_mut() { cb(uievent.clone()); } + self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.rendering.set(true); if !uievent.propagates.get() { return; @@ -418,9 +426,14 @@ 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 = el_ref.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + self.runtime.rendering.set(false); if let Some(cb) = listener.borrow_mut().as_deref_mut() { cb(uievent.clone()); } + self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.rendering.set(true); break; } @@ -501,10 +514,11 @@ impl VirtualDom { if sync.template.get().name.rsplit_once(':').unwrap().0 == template.name.rsplit_once(':').unwrap().0 { - let height = scope.height; + let context = scope.context(); + let height = context.height; self.dirty_scopes.insert(DirtyScope { height, - id: scope.id, + id: context.id, }); } } @@ -532,6 +546,7 @@ impl VirtualDom { /// apply_edits(edits); /// ``` pub fn rebuild(&mut self) -> Mutations { + let _runtime = RuntimeGuard::new(self.runtime.clone()); match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root RenderReturn::Ready(node) => { @@ -610,9 +625,12 @@ impl VirtualDom { continue; } - // Run the scope and get the mutations - self.run_scope(dirty.id); - self.diff_scope(dirty.id); + { + let _runtime = RuntimeGuard::new(self.runtime.clone()); + // Run the scope and get the mutations + self.run_scope(dirty.id); + self.diff_scope(dirty.id); + } } // If there's more work, then just continue, plenty of work to do diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 89ce50a48..53652f82f 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -158,8 +158,13 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { } let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { - Some(t) if t == "text/plain" => get_mime_by_ext(trimmed), - Some(f) => f, + Some(f) => { + if f == "text/plain" { + get_mime_by_ext(trimmed) + } else { + f + } + } None => get_mime_by_ext(trimmed), }; diff --git a/packages/dioxus-tui/examples/hover.rs b/packages/dioxus-tui/examples/hover.rs index 77770b46d..3af2d1616 100644 --- a/packages/dioxus-tui/examples/hover.rs +++ b/packages/dioxus-tui/examples/hover.rs @@ -1,6 +1,7 @@ use dioxus::{events::MouseData, prelude::*}; use dioxus_core::Event; use std::convert::TryInto; +use std::fmt::Write; use std::rc::Rc; fn main() { @@ -9,7 +10,12 @@ fn main() { fn app(cx: Scope) -> Element { fn to_str(c: &[i32; 3]) -> String { - "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::() + let mut result = String::new(); + result += "#"; + for c in c.iter() { + write!(result, "{c:02X?}").unwrap(); + } + result } fn get_brightness(m: &Rc) -> i32 { diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml new file mode 100644 index 000000000..6c5a5a841 --- /dev/null +++ b/packages/generational-box/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "generational-box" +authors = ["Evan Almloff"] +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bumpalo = { version = "3.6" } + +[dev-dependencies] +rand = "0.8.5" + +[features] +default = ["check_generation"] +check_generation = [] diff --git a/packages/generational-box/README.md b/packages/generational-box/README.md new file mode 100644 index 000000000..5ab7926ba --- /dev/null +++ b/packages/generational-box/README.md @@ -0,0 +1,34 @@ +# Generational Box + +Generational Box is a runtime for Rust that allows any static type to implement `Copy`. It can be combined with a global runtime to create an ergonomic state solution like `dioxus-signals`. This crate contains no `unsafe` code. + +Three main types manage state in Generational Box: + +- Store: Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread. +- Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped. +- GenerationalBox: The core Copy state type. The generational box will be dropped when the owner is dropped. + +Example: + +```rust +// Create a store for this thread +let store = Store::default(); + +{ + // Create an owner for some state for a scope + let owner = store.owner(); + + // Create some non-copy data, move it into a owner, and work with copy data + let data: String = "hello world".to_string(); + let key = owner.insert(data); + + // The generational box can be read from and written to like a RefCell + let value = key.read(); + assert_eq!(*value, "hello world"); +} +// Reading value at this point will cause a panic +``` + +## How it works + +Internally \ No newline at end of file diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs new file mode 100644 index 000000000..60bd41419 --- /dev/null +++ b/packages/generational-box/src/lib.rs @@ -0,0 +1,359 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + fmt::Debug, + marker::PhantomData, + rc::Rc, +}; + +use bumpalo::Bump; + +/// # Example +/// +/// ```compile_fail +/// let data = String::from("hello world"); +/// let store = Store::default(); +/// let owner = store.owner(); +/// let key = owner.insert(&data); +/// drop(data); +/// assert_eq!(*key.read(), "hello world"); +/// ``` +#[allow(unused)] +fn compile_fail() {} + +#[test] +fn reused() { + let store = Store::default(); + let first_ptr; + { + let owner = store.owner(); + first_ptr = owner.insert(1).raw.data.as_ptr(); + drop(owner); + } + { + let owner = store.owner(); + let second_ptr = owner.insert(1234).raw.data.as_ptr(); + assert_eq!(first_ptr, second_ptr); + drop(owner); + } +} + +#[test] +fn leaking_is_ok() { + let data = String::from("hello world"); + let store = Store::default(); + let key; + { + // create an owner + let owner = store.owner(); + // insert data into the store + key = owner.insert(data); + // don't drop the owner + std::mem::forget(owner); + } + assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string())); +} + +#[test] +fn drops() { + let data = String::from("hello world"); + let store = Store::default(); + let key; + { + // create an owner + let owner = store.owner(); + // insert data into the store + key = owner.insert(data); + // drop the owner + } + assert!(key.try_read().is_none()); +} + +#[test] +fn works() { + let store = Store::default(); + let owner = store.owner(); + let key = owner.insert(1); + + assert_eq!(*key.read(), 1); +} + +#[test] +fn insert_while_reading() { + let store = Store::default(); + let owner = store.owner(); + let key; + { + let data: String = "hello world".to_string(); + key = owner.insert(data); + } + let value = key.read(); + owner.insert(&1); + assert_eq!(*value, "hello world"); +} + +#[test] +#[should_panic] +fn panics() { + let store = Store::default(); + let owner = store.owner(); + let key = owner.insert(1); + drop(owner); + + assert_eq!(*key.read(), 1); +} + +#[test] +fn fuzz() { + fn maybe_owner_scope( + store: &Store, + valid_keys: &mut Vec>, + invalid_keys: &mut Vec>, + path: &mut Vec, + ) { + let branch_cutoff = 5; + let children = if path.len() < branch_cutoff { + rand::random::() % 4 + } else { + rand::random::() % 2 + }; + + for i in 0..children { + let owner = store.owner(); + let key = owner.insert(format!("hello world {path:?}")); + valid_keys.push(key); + path.push(i); + // read all keys + println!("{:?}", path); + for key in valid_keys.iter() { + let value = key.read(); + println!("{:?}", value); + assert!(value.starts_with("hello world")); + } + #[cfg(any(debug_assertions, feature = "check_generation"))] + for key in invalid_keys.iter() { + assert!(!key.validate()); + } + maybe_owner_scope(store, valid_keys, invalid_keys, path); + invalid_keys.push(valid_keys.pop().unwrap()); + path.pop(); + } + } + + for _ in 0..10 { + let store = Store::default(); + maybe_owner_scope(&store, &mut Vec::new(), &mut Vec::new(), &mut Vec::new()); + } +} + +/// The core Copy state type. The generational box will be dropped when the [Owner] is dropped. +pub struct GenerationalBox { + raw: MemoryLocation, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: u32, + _marker: PhantomData, +} + +impl Debug for GenerationalBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(any(debug_assertions, feature = "check_generation"))] + f.write_fmt(format_args!( + "{:?}@{:?}", + self.raw.data.as_ptr(), + self.generation + ))?; + #[cfg(not(any(debug_assertions, feature = "check_generation")))] + f.write_fmt(format_args!("{:?}", self.raw.data.as_ptr()))?; + Ok(()) + } +} + +impl GenerationalBox { + #[inline(always)] + fn validate(&self) -> bool { + #[cfg(any(debug_assertions, feature = "check_generation"))] + { + self.raw.generation.get() == self.generation + } + #[cfg(not(any(debug_assertions, feature = "check_generation")))] + { + true + } + } + + /// Try to read the value. Returns None if the value is no longer valid. + pub fn try_read(&self) -> Option> { + self.validate() + .then(|| { + Ref::filter_map(self.raw.data.borrow(), |any| { + any.as_ref()?.downcast_ref::() + }) + .ok() + }) + .flatten() + } + + /// Read the value. Panics if the value is no longer valid. + pub fn read(&self) -> Ref<'_, T> { + self.try_read().unwrap() + } + + /// Try to write the value. Returns None if the value is no longer valid. + pub fn try_write(&self) -> Option> { + self.validate() + .then(|| { + RefMut::filter_map(self.raw.data.borrow_mut(), |any| { + any.as_mut()?.downcast_mut::() + }) + .ok() + }) + .flatten() + } + + /// Write the value. Panics if the value is no longer valid. + pub fn write(&self) -> RefMut<'_, T> { + self.try_write().unwrap() + } + + /// Set the value. Panics if the value is no longer valid. + pub fn set(&self, value: T) { + self.validate().then(|| { + *self.raw.data.borrow_mut() = Some(Box::new(value)); + }); + } + + /// Returns true if the pointer is equal to the other pointer. + pub fn ptr_eq(&self, other: &Self) -> bool { + #[cfg(any(debug_assertions, feature = "check_generation"))] + { + self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation + } + #[cfg(not(any(debug_assertions, feature = "check_generation")))] + { + self.raw.data.as_ptr() == other.raw.data.as_ptr() + } + } +} + +impl Copy for GenerationalBox {} + +impl Clone for GenerationalBox { + fn clone(&self) -> Self { + *self + } +} + +#[derive(Clone, Copy)] +struct MemoryLocation { + data: &'static RefCell>>, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: &'static Cell, +} + +impl MemoryLocation { + #[allow(unused)] + fn drop(&self) { + let old = self.data.borrow_mut().take(); + #[cfg(any(debug_assertions, feature = "check_generation"))] + if old.is_some() { + drop(old); + let new_generation = self.generation.get() + 1; + self.generation.set(new_generation); + } + } + + fn replace(&mut self, value: T) -> GenerationalBox { + let mut inner_mut = self.data.borrow_mut(); + + let raw = Box::new(value); + let old = inner_mut.replace(raw); + assert!(old.is_none()); + GenerationalBox { + raw: *self, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: self.generation.get(), + _marker: PhantomData, + } + } +} + +/// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread. +#[derive(Clone)] +pub struct Store { + bump: &'static Bump, + recycled: Rc>>, +} + +impl Default for Store { + fn default() -> Self { + Self { + bump: Box::leak(Box::new(Bump::new())), + recycled: Default::default(), + } + } +} + +impl Store { + fn recycle(&self, location: MemoryLocation) { + location.drop(); + self.recycled.borrow_mut().push(location); + } + + fn claim(&self) -> MemoryLocation { + if let Some(location) = self.recycled.borrow_mut().pop() { + location + } else { + let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None)); + MemoryLocation { + data, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: self.bump.alloc(Cell::new(0)), + } + } + } + + /// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates. + pub fn owner(&self) -> Owner { + Owner { + store: self.clone(), + owned: Default::default(), + } + } +} + +/// Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped. +pub struct Owner { + store: Store, + owned: Rc>>, +} + +impl Owner { + /// Insert a value into the store. The value will be dropped when the owner is dropped. + pub fn insert(&self, value: T) -> GenerationalBox { + let mut location = self.store.claim(); + let key = location.replace(value); + self.owned.borrow_mut().push(location); + key + } + + /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value. + pub fn invalid(&self) -> GenerationalBox { + let location = self.store.claim(); + GenerationalBox { + raw: location, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: location.generation.get(), + _marker: PhantomData, + } + } +} + +impl Drop for Owner { + fn drop(&mut self) { + for location in self.owned.borrow().iter() { + self.store.recycle(*location) + } + } +} diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs index 94011d37d..1fcad8bb0 100644 --- a/packages/hooks/src/computed.rs +++ b/packages/hooks/src/computed.rs @@ -1,3 +1,5 @@ +//! Tracked and computed state in Dioxus + use dioxus_core::{ScopeId, ScopeState}; use slab::Slab; use std::{ diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 7e5798167..6d7ab6f53 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -52,8 +52,7 @@ macro_rules! to_owned { }; } -mod computed; -pub use computed::*; +pub mod computed; mod use_on_unmount; pub use use_on_unmount::*; diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index b39eef805..c90f5125b 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -42,7 +42,7 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator { Rc::new(move |script: &str| { eval_provider .new_evaluator(script.to_string()) - .map(|evaluator| UseEval::new(evaluator)) + .map(UseEval::new) }) as Rc Result> }) } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 44c378ddf..b520b2de1 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -446,7 +446,6 @@ impl RealDom { drop(tree); children.reverse(); if let Some(node) = self.get_mut(id) { - let node = node; f(node); stack.extend(children.iter()); } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 1f01fcd72..783dd3322 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -288,10 +288,7 @@ impl DynamicMapping { let idx = self.last_attribute_idx; self.last_attribute_idx += 1; - self.attribute_to_idx - .entry(attr) - .or_insert_with(Vec::new) - .push(idx); + self.attribute_to_idx.entry(attr).or_default().push(idx); idx } @@ -300,10 +297,7 @@ impl DynamicMapping { let idx = self.last_element_idx; self.last_element_idx += 1; - self.node_to_idx - .entry(node) - .or_insert_with(Vec::new) - .push(idx); + self.node_to_idx.entry(node).or_default().push(idx); idx } diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index a0c3534b0..0017a1545 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -8,4 +8,16 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } -slab = { workspace = true } +generational-box = { workspace = true } +log.workspace = true +simple_logger = "4.2.0" +serde = { version = "1", features = ["derive"], optional = true } + +[dev-dependencies] +dioxus = { workspace = true } +dioxus-desktop = { workspace = true } +tokio = { version = "1", features = ["full"] } + +[features] +default = [] +serialize = ["serde"] \ No newline at end of file diff --git a/packages/signals/README.md b/packages/signals/README.md new file mode 100644 index 000000000..020edc18f --- /dev/null +++ b/packages/signals/README.md @@ -0,0 +1,122 @@ +# Dioxus Signals + +Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions. + +## Copy Data + +All signals implement Copy, even if the inner value does not implement copy. This makes it easy to move any data into futures or children. + +```rust +use dioxus::prelude::*; +use dioxus_signals::*; + +fn app(cx: Scope) -> Element { + let signal = use_signal(cx, || "hello world".to_string()); + + spawn(async move { + // signal is Copy even though String is not copy + print!("{signal}"); + }); + + render! { + "{signal}" + } +} +``` + +## Local Subscriptions + +Signals will only subscribe to components when you read from the signal in that component. It will never subscribe to a component when reading data in a future or event handler. + +```rust +use dioxus::prelude::*; +use dioxus_signals::*; + +fn app(cx: Scope) -> Element { + // Because signal is never read in this component, this component will not rerun when the signal changes + let signal = use_signal(cx, || 0); + + render! { + button { + onclick: move |_| { + *signal.write() += 1; + }, + "Increase" + } + for id in 0..10 { + Child { + signal: signal, + } + } + } +} + +#[derive(Props, Clone, PartialEq)] +struct ChildProps { + signal: Signal, +} + +fn Child(cx: Scope) -> Element { + // This component does read from the signal, so when the signal changes it will rerun + render! { + "{cx.props.signal}" + } +} +``` + +Because subscriptions happen when you read from (not create) the data, you can provide signals through the normal context API: + +```rust +use dioxus::prelude::*; +use dioxus_signals::*; + +fn app(cx: Scope) -> Element { + // Because signal is never read in this component, this component will not rerun when the signal changes + use_context_provider(cx, || Signal::new(0)); + + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let signal: Signal = *use_context(cx).unwrap(); + // This component does read from the signal, so when the signal changes it will rerun + render! { + "{signal}" + } +} +``` + +## Computed Data + +In addition to local subscriptions in components, `dioxus-signals` provides a way to derive data with local subscriptions. + +The use_selector hook will only rerun when any signals inside of the hook change: + +```rust +use dioxus::prelude::*; +use dioxus_signals::*; + +fn app(cx: Scope) -> Element { + let signal = use_signal(cx, || 0); + let doubled = use_selector(cx, || signal * 2); + + render! { + button { + onclick: move |_| *signal.write() += 1, + "Increase" + } + Child { + signal: signal + } + } +} + +#[inline_props] +fn Child(cx: Scope, signal: ReadOnlySignal) -> Element { + render! { + "{signal}" + } +} +``` diff --git a/packages/signals/examples/context.rs b/packages/signals/examples/context.rs new file mode 100644 index 000000000..5e7ea831f --- /dev/null +++ b/packages/signals/examples/context.rs @@ -0,0 +1,25 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::Signal; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + // Because signal is never read in this component, this component will not rerun when the signal changes + use_context_provider(cx, || Signal::new(0)); + + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let signal: Signal = *use_context(cx).unwrap(); + // This component does read from the signal, so when the signal changes it will rerun + render! { + "{signal}" + } +} diff --git a/packages/signals/examples/dependancies.rs b/packages/signals/examples/dependancies.rs new file mode 100644 index 000000000..7b744eb3c --- /dev/null +++ b/packages/signals/examples/dependancies.rs @@ -0,0 +1,38 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let signal = use_signal(cx, || 0); + + use_future!(cx, || async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + *signal.write() += 1; + } + }); + + let local_state = use_state(cx, || 0); + let computed = + use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| { + local_state * 2 + signal.value() + }); + println!("Running app"); + + render! { + button { + onclick: move |_| { + local_state.set(local_state.get() + 1); + }, + "Add one" + } + div { + "{computed}" + } + } +} diff --git a/packages/signals/examples/selector.rs b/packages/signals/examples/selector.rs new file mode 100644 index 000000000..85d93ae7f --- /dev/null +++ b/packages/signals/examples/selector.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let signal = use_signal(cx, || 0); + let doubled = use_selector(cx, move || signal * 2); + + render! { + button { + onclick: move |_| *signal.write() += 1, + "Increase" + } + Child { + signal: doubled + } + } +} + +#[inline_props] +fn Child(cx: Scope, signal: ReadOnlySignal) -> Element { + render! { + "{signal}" + } +} diff --git a/packages/signals/examples/split_subscriptions.rs b/packages/signals/examples/split_subscriptions.rs new file mode 100644 index 000000000..3eb60ed29 --- /dev/null +++ b/packages/signals/examples/split_subscriptions.rs @@ -0,0 +1,150 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::Signal; + +fn main() { + dioxus_desktop::launch(app); +} + +#[derive(Clone, Copy, Default)] +struct ApplicationData { + first_data: Signal, + second_data: Signal, + many_signals: Signal>>, +} + +fn use_app_data(cx: Scope) -> ApplicationData { + *use_context(cx).unwrap() +} + +fn app(cx: Scope) -> Element { + use_context_provider(cx, ApplicationData::default); + + render! { + div { + ReadsFirst {} + } + div { + ReadsSecond {} + } + div { + ReadsManySignals {} + } + } +} + +fn ReadsFirst(cx: Scope) -> Element { + println!("running first"); + let data = use_app_data(cx); + + render! { + button { + onclick: move |_| { + *data.first_data.write() += 1; + }, + "Increase" + } + button { + onclick: move |_| { + *data.first_data.write() -= 1; + }, + "Decrease" + } + button { + onclick: move |_| { + *data.first_data.write() = 0; + }, + "Reset" + } + "{data.first_data}" + } +} + +fn ReadsSecond(cx: Scope) -> Element { + println!("running second"); + let data = use_app_data(cx); + + render! { + button { + onclick: move |_| { + *data.second_data.write() += 1; + }, + "Increase" + } + button { + onclick: move |_| { + *data.second_data.write() -= 1; + }, + "Decrease" + } + button { + onclick: move |_| { + *data.second_data.write() = 0; + }, + "Reset" + } + "{data.second_data}" + } +} + +fn ReadsManySignals(cx: Scope) -> Element { + println!("running many signals"); + let data = use_app_data(cx); + + render! { + button { + onclick: move |_| { + data.many_signals.write().push(Signal::new(0)); + }, + "Create" + } + button { + onclick: move |_| { + data.many_signals.write().pop(); + }, + "Destroy" + } + button { + onclick: move |_| { + if let Some(first) = data.many_signals.read().get(0) { + *first.write() += 1; + } + }, + "Increase First Item" + } + for signal in data.many_signals { + Child { + count: signal, + } + } + } +} + +#[derive(Props, PartialEq)] +struct ChildProps { + count: Signal, +} + +fn Child(cx: Scope) -> Element { + println!("running child"); + let count = cx.props.count; + + render! { + div { + "Child: {count}" + button { + onclick: move |_| { + *count.write() += 1; + }, + "Increase" + } + button { + onclick: move |_| { + *count.write() -= 1; + }, + "Decrease" + } + } + } +} diff --git a/packages/signals/src/dependency.rs b/packages/signals/src/dependency.rs new file mode 100644 index 000000000..d6ab65a5f --- /dev/null +++ b/packages/signals/src/dependency.rs @@ -0,0 +1,67 @@ +/// A dependency is a trait that can be used to determine if a effect or selector should be re-run. +pub trait Dependency: Sized + Clone { + /// The output of the dependency + type Out: Clone + PartialEq; + /// Returns the output of the dependency. + fn out(&self) -> Self::Out; + /// Returns true if the dependency has changed. + fn changed(&self, other: &Self::Out) -> bool { + self.out() != *other + } +} + +impl Dependency for () { + type Out = (); + fn out(&self) -> Self::Out {} +} + +/// A dependency is a trait that can be used to determine if a effect or selector should be re-run. +pub trait Dep: 'static + PartialEq + Clone {} +impl Dep for T where T: 'static + PartialEq + Clone {} + +impl Dependency for &A { + type Out = A; + fn out(&self) -> Self::Out { + (*self).clone() + } +} + +macro_rules! impl_dep { + ( + $($el:ident=$name:ident $other:ident,)* + ) => { + impl< $($el),* > Dependency for ($(&$el,)*) + where + $( + $el: Dep + ),* + { + type Out = ($($el,)*); + + fn out(&self) -> Self::Out { + let ($($name,)*) = self; + ($((*$name).clone(),)*) + } + + fn changed(&self, other: &Self::Out) -> bool { + let ($($name,)*) = self; + let ($($other,)*) = other; + $( + if *$name != $other { + return true; + } + )* + false + } + } + }; +} + +impl_dep!(A = a1 a2,); +impl_dep!(A = a1 a2, B = b1 b2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2, H = h1 h2,); diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs new file mode 100644 index 000000000..0685938a0 --- /dev/null +++ b/packages/signals/src/effect.rs @@ -0,0 +1,96 @@ +use core::{self, fmt::Debug}; +use std::cell::RefCell; +use std::fmt::{self, Formatter}; +use std::rc::Rc; +// +use dioxus_core::prelude::*; + +use crate::use_signal; +use crate::{dependency::Dependency, CopyValue}; + +#[derive(Default, Clone)] +pub(crate) struct EffectStack { + pub(crate) effects: Rc>>, +} + +pub(crate) fn get_effect_stack() -> EffectStack { + match consume_context() { + Some(rt) => rt, + None => { + let store = EffectStack::default(); + provide_root_context(store).expect("in a virtual dom") + } + } +} + +/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. +/// The signal will be owned by the current component and will be dropped when the component is dropped. +pub fn use_effect(cx: &ScopeState, callback: impl FnMut() + 'static) { + cx.use_hook(|| Effect::new(callback)); +} + +/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. +/// The signal will be owned by the current component and will be dropped when the component is dropped. +pub fn use_effect_with_dependencies( + cx: &ScopeState, + dependencies: D, + mut callback: impl FnMut(D::Out) + 'static, +) where + D::Out: 'static, +{ + let dependencies_signal = use_signal(cx, || dependencies.out()); + cx.use_hook(|| { + Effect::new(move || { + let deref = &*dependencies_signal.read(); + callback(deref.clone()); + }); + }); + let changed = { dependencies.changed(&*dependencies_signal.read()) }; + if changed { + dependencies_signal.set(dependencies.out()); + } +} + +/// Effects allow you to run code when a signal changes. Effects are run immediately and whenever any signal it reads changes. +#[derive(Copy, Clone, PartialEq)] +pub struct Effect { + pub(crate) callback: CopyValue>, +} + +impl Debug for Effect { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:?}", self.callback.value)) + } +} + +impl Effect { + pub(crate) fn current() -> Option { + get_effect_stack().effects.borrow().last().copied() + } + + /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. + /// + /// The signal will be owned by the current component and will be dropped when the component is dropped. + pub fn new(callback: impl FnMut() + 'static) -> Self { + let myself = Self { + callback: CopyValue::new(Box::new(callback)), + }; + + myself.try_run(); + + myself + } + + /// Run the effect callback immediately. Returns `true` if the effect was run. Returns `false` is the effect is dead. + pub fn try_run(&self) { + if let Some(mut callback) = self.callback.try_write() { + { + get_effect_stack().effects.borrow_mut().push(*self); + } + callback(); + { + get_effect_stack().effects.borrow_mut().pop(); + } + } + } +} diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs new file mode 100644 index 000000000..6427c2e2b --- /dev/null +++ b/packages/signals/src/impls.rs @@ -0,0 +1,294 @@ +use crate::rt::CopyValue; +use crate::signal::{ReadOnlySignal, Signal, Write}; + +use std::cell::{Ref, RefMut}; + +use std::{ + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; + +macro_rules! read_impls { + ($ty:ident) => { + impl Default for $ty { + fn default() -> Self { + Self::new(Default::default()) + } + } + + impl std::clone::Clone for $ty { + fn clone(&self) -> Self { + *self + } + } + + impl Copy for $ty {} + + impl Display for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.with(|v| Display::fmt(v, f)) + } + } + + impl Debug for $ty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.with(|v| Debug::fmt(v, f)) + } + } + + impl $ty> { + /// Read a value from the inner vector. + pub fn get(&self, index: usize) -> Option> { + Ref::filter_map(self.read(), |v| v.get(index)).ok() + } + } + + impl $ty> { + /// Unwraps the inner value and clones it. + pub fn unwrap(&self) -> T + where + T: Clone, + { + self.with(|v| v.clone()).unwrap() + } + + /// Attemps to read the inner value of the Option. + pub fn as_ref(&self) -> Option> { + Ref::filter_map(self.read(), |v| v.as_ref()).ok() + } + } + }; +} + +macro_rules! write_impls { + ($ty:ident) => { + impl + Copy + 'static> std::ops::Add for $ty { + type Output = T; + + fn add(self, rhs: T) -> Self::Output { + self.with(|v| *v + rhs) + } + } + + impl + Copy + 'static> std::ops::AddAssign for $ty { + fn add_assign(&mut self, rhs: T) { + self.with_mut(|v| *v = *v + rhs) + } + } + + impl + Copy + 'static> std::ops::SubAssign for $ty { + fn sub_assign(&mut self, rhs: T) { + self.with_mut(|v| *v = *v - rhs) + } + } + + impl + Copy + 'static> std::ops::Sub for $ty { + type Output = T; + + fn sub(self, rhs: T) -> Self::Output { + self.with(|v| *v - rhs) + } + } + + impl + Copy + 'static> std::ops::MulAssign for $ty { + fn mul_assign(&mut self, rhs: T) { + self.with_mut(|v| *v = *v * rhs) + } + } + + impl + Copy + 'static> std::ops::Mul for $ty { + type Output = T; + + fn mul(self, rhs: T) -> Self::Output { + self.with(|v| *v * rhs) + } + } + + impl + Copy + 'static> std::ops::DivAssign for $ty { + fn div_assign(&mut self, rhs: T) { + self.with_mut(|v| *v = *v / rhs) + } + } + + impl + Copy + 'static> std::ops::Div for $ty { + type Output = T; + + fn div(self, rhs: T) -> Self::Output { + self.with(|v| *v / rhs) + } + } + + impl $ty> { + /// Pushes a new value to the end of the vector. + pub fn push(&self, value: T) { + self.with_mut(|v| v.push(value)) + } + + /// Pops the last value from the vector. + pub fn pop(&self) -> Option { + self.with_mut(|v| v.pop()) + } + + /// Inserts a new value at the given index. + pub fn insert(&self, index: usize, value: T) { + self.with_mut(|v| v.insert(index, value)) + } + + /// Removes the value at the given index. + pub fn remove(&self, index: usize) -> T { + self.with_mut(|v| v.remove(index)) + } + + /// Clears the vector, removing all values. + pub fn clear(&self) { + self.with_mut(|v| v.clear()) + } + + /// Extends the vector with the given iterator. + pub fn extend(&self, iter: impl IntoIterator) { + self.with_mut(|v| v.extend(iter)) + } + + /// Truncates the vector to the given length. + pub fn truncate(&self, len: usize) { + self.with_mut(|v| v.truncate(len)) + } + + /// Swaps two values in the vector. + pub fn swap_remove(&self, index: usize) -> T { + self.with_mut(|v| v.swap_remove(index)) + } + + /// Retains only the values that match the given predicate. + pub fn retain(&self, f: impl FnMut(&T) -> bool) { + self.with_mut(|v| v.retain(f)) + } + + /// Splits the vector into two at the given index. + pub fn split_off(&self, at: usize) -> Vec { + self.with_mut(|v| v.split_off(at)) + } + } + + impl $ty> { + /// Takes the value out of the Option. + pub fn take(&self) -> Option { + self.with_mut(|v| v.take()) + } + + /// Replace the value in the Option. + pub fn replace(&self, value: T) -> Option { + self.with_mut(|v| v.replace(value)) + } + + /// Gets the value out of the Option, or inserts the given value if the Option is empty. + pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { + self.get_or_insert_with(|| default) + } + + /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty. + pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> { + let borrow = self.read(); + if borrow.is_none() { + drop(borrow); + self.with_mut(|v| *v = Some(default())); + Ref::map(self.read(), |v| v.as_ref().unwrap()) + } else { + Ref::map(borrow, |v| v.as_ref().unwrap()) + } + } + } + }; +} + +read_impls!(CopyValue); +write_impls!(CopyValue); +read_impls!(Signal); +write_impls!(Signal); +read_impls!(ReadOnlySignal); + +/// An iterator over the values of a `CopyValue>`. +pub struct CopyValueIterator { + index: usize, + value: CopyValue>, +} + +impl Iterator for CopyValueIterator { + type Item = T; + + fn next(&mut self) -> Option { + let index = self.index; + self.index += 1; + self.value.get(index).map(|v| v.clone()) + } +} + +impl IntoIterator for CopyValue> { + type IntoIter = CopyValueIterator; + + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + CopyValueIterator { + index: 0, + value: self, + } + } +} + +impl CopyValue> { + /// Write to an element in the inner vector. + pub fn get_mut(&self, index: usize) -> Option> { + RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() + } +} + +impl CopyValue> { + /// Deref the inner value mutably. + pub fn as_mut(&self) -> Option> { + RefMut::filter_map(self.write(), |v| v.as_mut()).ok() + } +} + +/// An iterator over items in a `Signal>`. +pub struct SignalIterator { + index: usize, + value: Signal>, +} + +impl Iterator for SignalIterator { + type Item = T; + + fn next(&mut self) -> Option { + let index = self.index; + self.index += 1; + self.value.get(index).map(|v| v.clone()) + } +} + +impl IntoIterator for Signal> { + type IntoIter = SignalIterator; + + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + SignalIterator { + index: 0, + value: self, + } + } +} + +impl Signal> { + /// Returns a reference to an element or `None` if out of bounds. + pub fn get_mut(&self, index: usize) -> Option>> { + Write::filter_map(self.write(), |v| v.get_mut(index)) + } +} + +impl Signal> { + /// Returns a reference to an element or `None` if out of bounds. + pub fn as_mut(&self) -> Option>> { + Write::filter_map(self.write(), |v| v.as_mut()) + } +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 5b6a740cf..1f8bc3297 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,131 +1,14 @@ -use std::{ - cell::{Ref, RefMut}, - fmt::Display, - marker::PhantomData, - ops::{Add, Div, Mul, Sub}, -}; +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] mod rt; - -use dioxus_core::ScopeState; pub use rt::*; - -pub fn use_init_signal_rt(cx: &ScopeState) { - cx.use_hook(|| { - let rt = claim_rt(cx.schedule_update_any()); - cx.provide_context(rt); - }); -} - -pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { - cx.use_hook(|| { - let rt: &'static SignalRt = match cx.consume_context() { - Some(rt) => rt, - None => cx.provide_context(claim_rt(cx.schedule_update_any())), - }; - - let id = rt.init(f()); - rt.subscribe(id, cx.scope_id()); - - struct SignalHook { - signal: Signal, - } - - impl Drop for SignalHook { - fn drop(&mut self) { - self.signal.rt.remove(self.signal.id); - } - } - - SignalHook { - signal: Signal { - id, - rt, - t: PhantomData, - }, - } - }) - .signal -} - -pub struct Signal { - id: usize, - rt: &'static SignalRt, - t: PhantomData, -} - -impl Signal { - pub fn read(&self) -> Ref { - self.rt.read(self.id) - } - - pub fn write(&self) -> RefMut { - self.rt.write(self.id) - } - - pub fn set(&mut self, value: T) { - self.rt.set(self.id, value); - } - - pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { - let write = self.read(); - f(&*write) - } - - pub fn update(&self, _f: impl FnOnce(&mut T) -> O) -> O { - let mut write = self.write(); - _f(&mut *write) - } -} - -impl Signal { - pub fn get(&self) -> T { - self.rt.get(self.id) - } -} - -impl std::ops::Deref for Signal { - type Target = dyn Fn() -> T; - - fn deref(&self) -> &Self::Target { - self.rt.getter(self.id) - } -} - -impl std::clone::Clone for Signal { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Signal {} - -impl Display for Signal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.rt.with::(self.id, |v| T::fmt(v, f)) - } -} - -impl + Copy + 'static> std::ops::AddAssign for Signal { - fn add_assign(&mut self, rhs: T) { - self.set(self.get() + rhs); - } -} - -impl + Copy + 'static> std::ops::SubAssign for Signal { - fn sub_assign(&mut self, rhs: T) { - self.set(self.get() - rhs); - } -} - -impl + Copy + 'static> std::ops::MulAssign for Signal { - fn mul_assign(&mut self, rhs: T) { - self.set(self.get() * rhs); - } -} - -impl + Copy + 'static> std::ops::DivAssign for Signal { - fn div_assign(&mut self, rhs: T) { - self.set(self.get() / rhs); - } -} +mod effect; +pub use effect::*; +mod impls; +mod selector; +pub use selector::*; +pub(crate) mod signal; +pub use signal::*; +mod dependency; +pub use dependency::*; diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index e849f9008..7322d43b4 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -1,121 +1,159 @@ -use std::{any::Any, cell::RefCell, sync::Arc}; +use std::cell::{Ref, RefMut}; +use std::rc::Rc; + +use dioxus_core::prelude::{ + consume_context, consume_context_from_scope, current_scope_id, provide_context, + provide_context_to_scope, provide_root_context, +}; use dioxus_core::ScopeId; -use slab::Slab; -thread_local! { - // we cannot drop these since any future might be using them - static RUNTIMES: RefCell> = RefCell::new(Vec::new()); +use generational_box::{GenerationalBox, Owner, Store}; + +fn current_store() -> Store { + match consume_context() { + Some(rt) => rt, + None => { + let store = Store::default(); + provide_root_context(store).expect("in a virtual dom") + } + } } -/// Provide the runtime for signals +fn current_owner() -> Rc { + match consume_context() { + Some(rt) => rt, + None => { + let owner = Rc::new(current_store().owner()); + provide_context(owner).expect("in a virtual dom") + } + } +} + +fn owner_in_scope(scope: ScopeId) -> Rc { + match consume_context_from_scope(scope) { + Some(rt) => rt, + None => { + let owner = Rc::new(current_store().owner()); + provide_context_to_scope(scope, owner).expect("in a virtual dom") + } + } +} + +/// CopyValue is a wrapper around a value to make the value mutable and Copy. /// -/// This will reuse dead runtimes -pub fn claim_rt(update_any: Arc) -> &'static SignalRt { - RUNTIMES.with(|runtimes| { - if let Some(rt) = runtimes.borrow_mut().pop() { - return rt; - } - - Box::leak(Box::new(SignalRt { - signals: RefCell::new(Slab::new()), - update_any, - })) - }) +/// It is internally backed by [`generational_box::GenerationalBox`]. +pub struct CopyValue { + pub(crate) value: GenerationalBox, + origin_scope: ScopeId, } -/// Push this runtime into the global runtime list -pub fn reclam_rt(_rt: &'static SignalRt) { - RUNTIMES.with(|runtimes| { - runtimes.borrow_mut().push(_rt); - }); +#[cfg(feature = "serde")] +impl serde::Serialize for CopyValue +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result { + self.value.read().serialize(serializer) + } } -pub struct SignalRt { - pub(crate) signals: RefCell>, - pub(crate) update_any: Arc, +#[cfg(feature = "serde")] +impl<'de, T: 'static> serde::Deserialize<'de> for CopyValue +where + T: serde::Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let value = T::deserialize(deserializer)?; + + Ok(Self::new(value)) + } } -impl SignalRt { - pub fn init(&'static self, val: T) -> usize { - self.signals.borrow_mut().insert(Inner { - value: Box::new(val), - subscribers: Vec::new(), - getter: None, - }) - } +impl CopyValue { + /// Create a new CopyValue. The value will be stored in the current component. + /// + /// Once the component this value is created in is dropped, the value will be dropped. + pub fn new(value: T) -> Self { + let owner = current_owner(); - pub fn subscribe(&self, id: usize, subscriber: ScopeId) { - self.signals.borrow_mut()[id].subscribers.push(subscriber); - } - - pub fn get(&self, id: usize) -> T { - self.signals.borrow()[id] - .value - .downcast_ref::() - .cloned() - .unwrap() - } - - pub fn set(&self, id: usize, value: T) { - let mut signals = self.signals.borrow_mut(); - let inner = &mut signals[id]; - inner.value = Box::new(value); - - for subscriber in inner.subscribers.iter() { - (self.update_any)(*subscriber); + Self { + value: owner.insert(value), + origin_scope: current_scope_id().expect("in a virtual dom"), } } - pub fn remove(&self, id: usize) { - self.signals.borrow_mut().remove(id); - } + /// Create a new CopyValue. The value will be stored in the given scope. When the specified scope is dropped, the value will be dropped. + pub fn new_in_scope(value: T, scope: ScopeId) -> Self { + let owner = owner_in_scope(scope); - pub fn with(&self, id: usize, f: impl FnOnce(&T) -> O) -> O { - let signals = self.signals.borrow(); - let inner = &signals[id]; - let inner = inner.value.downcast_ref::().unwrap(); - f(inner) - } - - pub(crate) fn read(&self, id: usize) -> std::cell::Ref { - let signals = self.signals.borrow(); - std::cell::Ref::map(signals, |signals| { - signals[id].value.downcast_ref::().unwrap() - }) - } - - pub(crate) fn write(&self, id: usize) -> std::cell::RefMut { - let signals = self.signals.borrow_mut(); - std::cell::RefMut::map(signals, |signals| { - signals[id].value.downcast_mut::().unwrap() - }) - } - - pub(crate) fn getter(&self, id: usize) -> &dyn Fn() -> T { - let mut signals = self.signals.borrow_mut(); - let inner = &mut signals[id]; - let r = inner.getter.as_mut(); - - if r.is_none() { - let rt = self; - let r = move || rt.get::(id); - let getter: Box T> = Box::new(r); - let getter: Box = unsafe { std::mem::transmute(getter) }; - - inner.getter = Some(getter); + Self { + value: owner.insert(value), + origin_scope: scope, } + } - let r = inner.getter.as_ref().unwrap(); + pub(crate) fn invalid() -> Self { + let owner = current_owner(); - unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) } + Self { + value: owner.invalid(), + origin_scope: current_scope_id().expect("in a virtual dom"), + } + } + + /// Get the scope this value was created in. + pub fn origin_scope(&self) -> ScopeId { + self.origin_scope + } + + /// Try to read the value. If the value has been dropped, this will return None. + pub fn try_read(&self) -> Option> { + self.value.try_read() + } + + /// Read the value. If the value has been dropped, this will panic. + pub fn read(&self) -> Ref<'_, T> { + self.value.read() + } + + /// Try to write the value. If the value has been dropped, this will return None. + pub fn try_write(&self) -> Option> { + self.value.try_write() + } + + /// Write the value. If the value has been dropped, this will panic. + pub fn write(&self) -> RefMut<'_, T> { + self.value.write() + } + + /// Set the value. If the value has been dropped, this will panic. + pub fn set(&mut self, value: T) { + *self.write() = value; + } + + /// Run a function with a reference to the value. If the value has been dropped, this will panic. + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + /// Run a function with a mutable reference to the value. If the value has been dropped, this will panic. + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + f(&mut *write) } } -pub(crate) struct Inner { - pub value: Box, - pub subscribers: Vec, +impl CopyValue { + /// Get the value. If the value has been dropped, this will panic. + pub fn value(&self) -> T { + self.read().clone() + } +} - // todo: this has a soundness hole in it that you might not run into - pub getter: Option>, +impl PartialEq for CopyValue { + fn eq(&self, other: &Self) -> bool { + self.value.ptr_eq(&other.value) + } } diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs new file mode 100644 index 000000000..5eb3fa3c9 --- /dev/null +++ b/packages/signals/src/selector.rs @@ -0,0 +1,105 @@ +use dioxus_core::prelude::*; + +use crate::dependency::Dependency; +use crate::use_signal; +use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySignal, Signal}; + +/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes. +/// +/// Selectors can be used to efficiently compute derived data from signals. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut count = use_signal(cx, || 0); +/// let double = use_selector(cx, move || count * 2); +/// count += 1; +/// assert_eq!(double.value(), count * 2); +/// +/// render! { "{double}" } +/// } +/// ``` +pub fn use_selector( + cx: &ScopeState, + f: impl FnMut() -> R + 'static, +) -> ReadOnlySignal { + *cx.use_hook(|| selector(f)) +} + +/// Creates a new Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes +/// +/// Selectors can be used to efficiently compute derived data from signals. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut local_state = use_state(cx, || 0); +/// let double = use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2); +/// local_state.set(1); +/// +/// render! { "{double}" } +/// } +/// ``` +pub fn use_selector_with_dependencies( + cx: &ScopeState, + dependencies: D, + mut f: impl FnMut(D::Out) -> R + 'static, +) -> ReadOnlySignal +where + D::Out: 'static, +{ + let dependencies_signal = use_signal(cx, || dependencies.out()); + let selector = *cx.use_hook(|| { + selector(move || { + let deref = &*dependencies_signal.read(); + f(deref.clone()) + }) + }); + let changed = { dependencies.changed(&*dependencies_signal.read()) }; + if changed { + dependencies_signal.set(dependencies.out()); + } + selector +} + +/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes. +/// +/// Selectors can be used to efficiently compute derived data from signals. +pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal { + let state = Signal:: { + inner: CopyValue::invalid(), + }; + let effect = Effect { + callback: CopyValue::invalid(), + }; + + { + get_effect_stack().effects.borrow_mut().push(effect); + } + state.inner.value.set(SignalData { + subscribers: Default::default(), + effect_subscribers: Default::default(), + update_any: schedule_update_any().expect("in a virtual dom"), + value: f(), + }); + { + get_effect_stack().effects.borrow_mut().pop(); + } + + effect.callback.value.set(Box::new(move || { + let value = f(); + let changed = { + let old = state.inner.read(); + value != old.value + }; + if changed { + state.set(value) + } + })); + + ReadOnlySignal::new(state) +} diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs new file mode 100644 index 000000000..a5599a1ce --- /dev/null +++ b/packages/signals/src/signal.rs @@ -0,0 +1,344 @@ +use std::{ + cell::{Ref, RefCell, RefMut}, + ops::{Deref, DerefMut}, + rc::Rc, + sync::Arc, +}; + +use dioxus_core::{ + prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, + ScopeId, ScopeState, +}; + +use crate::{CopyValue, Effect}; + +/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut count = use_signal(cx, || 0); +/// +/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated. +/// // The app component will never be rerendered in this example. +/// render! { Child { state: count } } +/// } +/// +/// #[inline_props] +/// fn Child(cx: Scope, state: Signal) -> Element { +/// let state = *state; +/// +/// use_future!(cx, |()| async move { +/// // Because the signal is a Copy type, we can use it in an async block without cloning it. +/// *state.write() += 1; +/// }); +/// +/// render! { +/// button { +/// onclick: move |_| *state.write() += 1, +/// "{state}" +/// } +/// } +/// } +/// ``` +pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { + *cx.use_hook(|| Signal::new(f())) +} + +#[derive(Clone)] +struct Unsubscriber { + scope: ScopeId, + subscribers: UnsubscriberArray, +} + +type UnsubscriberArray = Rc>>>>>; + +impl Drop for Unsubscriber { + fn drop(&mut self) { + for subscribers in self.subscribers.borrow().iter() { + subscribers.borrow_mut().retain(|s| *s != self.scope); + } + } +} + +fn current_unsubscriber() -> Unsubscriber { + match has_context() { + Some(rt) => rt, + None => { + let owner = Unsubscriber { + scope: current_scope_id().expect("in a virtual dom"), + subscribers: Default::default(), + }; + provide_context(owner).expect("in a virtual dom") + } + } +} + +pub(crate) struct SignalData { + pub(crate) subscribers: Rc>>, + pub(crate) effect_subscribers: Rc>>, + pub(crate) update_any: Arc, + pub(crate) value: T, +} + +/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut count = use_signal(cx, || 0); +/// +/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated. +/// // The app component will never be rerendered in this example. +/// render! { Child { state: count } } +/// } +/// +/// #[inline_props] +/// fn Child(cx: Scope, state: Signal) -> Element { +/// let state = *state; +/// +/// use_future!(cx, |()| async move { +/// // Because the signal is a Copy type, we can use it in an async block without cloning it. +/// *state.write() += 1; +/// }); +/// +/// render! { +/// button { +/// onclick: move |_| *state.write() += 1, +/// "{state}" +/// } +/// } +/// } +/// ``` +pub struct Signal { + pub(crate) inner: CopyValue>, +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Signal { + fn serialize(&self, serializer: S) -> Result { + self.read().serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, T: serde::Deserialize<'de> + 'static> serde::Deserialize<'de> for Signal { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(T::deserialize(deserializer)?)) + } +} + +impl Signal { + /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. + pub fn new(value: T) -> Self { + Self { + inner: CopyValue::new(SignalData { + subscribers: Default::default(), + effect_subscribers: Default::default(), + update_any: schedule_update_any().expect("in a virtual dom"), + value, + }), + } + } + + /// Get the scope the signal was created in. + pub fn origin_scope(&self) -> ScopeId { + self.inner.origin_scope() + } + + /// Get the current value of the signal. This will subscribe the current scope to the signal. + /// If the signal has been dropped, this will panic. + pub fn read(&self) -> Ref { + let inner = self.inner.read(); + if let Some(effect) = Effect::current() { + let mut effect_subscribers = inner.effect_subscribers.borrow_mut(); + if !effect_subscribers.contains(&effect) { + effect_subscribers.push(effect); + } + } else if let Some(current_scope_id) = current_scope_id() { + // only subscribe if the vdom is rendering + if dioxus_core::vdom_is_rendering() { + log::trace!( + "{:?} subscribed to {:?}", + self.inner.value, + current_scope_id + ); + let mut subscribers = inner.subscribers.borrow_mut(); + if !subscribers.contains(¤t_scope_id) { + subscribers.push(current_scope_id); + drop(subscribers); + let unsubscriber = current_unsubscriber(); + inner.subscribers.borrow_mut().push(unsubscriber.scope); + } + } + } + Ref::map(inner, |v| &v.value) + } + + /// Get a mutable reference to the signal's value. + /// If the signal has been dropped, this will panic. + pub fn write(&self) -> Write<'_, T> { + let inner = self.inner.write(); + let borrow = RefMut::map(inner, |v| &mut v.value); + Write { + write: borrow, + signal: SignalSubscriberDrop { signal: *self }, + } + } + + fn update_subscribers(&self) { + { + let inner = self.inner.read(); + for &scope_id in &*inner.subscribers.borrow() { + log::trace!( + "Write on {:?} triggered update on {:?}", + self.inner.value, + scope_id + ); + (inner.update_any)(scope_id); + } + } + + let subscribers = { + let self_read = self.inner.read(); + let mut effects = self_read.effect_subscribers.borrow_mut(); + std::mem::take(&mut *effects) + }; + for effect in subscribers { + log::trace!( + "Write on {:?} triggered effect {:?}", + self.inner.value, + effect + ); + effect.try_run(); + } + } + + /// Set the value of the signal. This will trigger an update on all subscribers. + pub fn set(&self, value: T) { + *self.write() = value; + } + + /// Run a closure with a reference to the signal's value. + /// If the signal has been dropped, this will panic. + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + /// Run a closure with a mutable reference to the signal's value. + /// If the signal has been dropped, this will panic. + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + f(&mut *write) + } +} + +impl Signal { + /// Get the current value of the signal. This will subscribe the current scope to the signal. + /// If the signal has been dropped, this will panic. + pub fn value(&self) -> T { + self.read().clone() + } +} + +impl PartialEq for Signal { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +struct SignalSubscriberDrop { + signal: Signal, +} + +impl Drop for SignalSubscriberDrop { + fn drop(&mut self) { + self.signal.update_subscribers(); + } +} + +/// A mutable reference to a signal's value. +pub struct Write<'a, T: 'static, I: 'static = T> { + write: RefMut<'a, T>, + signal: SignalSubscriberDrop, +} + +impl<'a, T: 'static, I: 'static> Write<'a, T, I> { + /// Map the mutable reference to the signal's value to a new type. + pub fn map(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, I> { + let Self { write, signal } = myself; + Write { + write: RefMut::map(write, f), + signal, + } + } + + /// Try to map the mutable reference to the signal's value to a new type + pub fn filter_map( + myself: Self, + f: impl FnOnce(&mut T) -> Option<&mut O>, + ) -> Option> { + let Self { write, signal } = myself; + let write = RefMut::filter_map(write, f).ok(); + write.map(|write| Write { write, signal }) + } +} + +impl<'a, T: 'static> Deref for Write<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.write + } +} + +impl DerefMut for Write<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.write + } +} + +/// A signal that can only be read from. +pub struct ReadOnlySignal { + inner: Signal, +} + +impl ReadOnlySignal { + /// Create a new read-only signal. + pub fn new(signal: Signal) -> Self { + Self { inner: signal } + } + + /// Get the scope that the signal was created in. + pub fn origin_scope(&self) -> ScopeId { + self.inner.origin_scope() + } + + /// Get the current value of the signal. This will subscribe the current scope to the signal. + pub fn read(&self) -> Ref { + self.inner.read() + } + + /// Run a closure with a reference to the signal's value. + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + self.inner.with(f) + } +} + +impl ReadOnlySignal { + /// Get the current value of the signal. This will subscribe the current scope to the signal. + pub fn value(&self) -> T { + self.read().clone() + } +} + +impl PartialEq for ReadOnlySignal { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} diff --git a/packages/signals/tests/create.rs b/packages/signals/tests/create.rs new file mode 100644 index 000000000..0681263ea --- /dev/null +++ b/packages/signals/tests/create.rs @@ -0,0 +1,60 @@ +#![allow(unused, non_upper_case_globals, non_snake_case)] + +use dioxus::prelude::*; +use dioxus_core::ElementId; +use dioxus_signals::*; + +#[test] +fn create_signals_global() { + let mut dom = VirtualDom::new(|cx| { + render! { + for _ in 0..10 { + Child {} + } + } + }); + + fn Child(cx: Scope) -> Element { + let signal = create_without_cx(); + + render! { + "{signal}" + } + } + + let _edits = dom.rebuild().santize(); + + fn create_without_cx() -> Signal { + Signal::new("hello world".to_string()) + } +} + +#[test] +fn drop_signals() { + let mut dom = VirtualDom::new(|cx| { + let generation = cx.generation(); + + let count = if generation % 2 == 0 { 10 } else { 0 }; + render! { + for _ in 0..count { + Child {} + } + } + }); + + fn Child(cx: Scope) -> Element { + let signal = create_without_cx(); + + render! { + "{signal}" + } + } + + let _ = dom.rebuild().santize(); + dom.mark_dirty(ScopeId(0)); + dom.render_immediate(); + + fn create_without_cx() -> Signal { + Signal::new("hello world".to_string()) + } +} diff --git a/packages/signals/tests/effect.rs b/packages/signals/tests/effect.rs new file mode 100644 index 000000000..513b7e929 --- /dev/null +++ b/packages/signals/tests/effect.rs @@ -0,0 +1,47 @@ +#![allow(unused, non_upper_case_globals, non_snake_case)] +use std::collections::HashMap; +use std::rc::Rc; + +use dioxus::prelude::*; +use dioxus_core::ElementId; +use dioxus_signals::*; + +#[test] +fn effects_rerun() { + simple_logger::SimpleLogger::new().init().unwrap(); + + #[derive(Default)] + struct RunCounter { + component: usize, + effect: usize, + } + + let counter = Rc::new(RefCell::new(RunCounter::default())); + let mut dom = VirtualDom::new_with_props( + |cx| { + let counter = cx.props; + counter.borrow_mut().component += 1; + + let mut signal = use_signal(cx, || 0); + cx.use_hook(move || { + to_owned![counter]; + Effect::new(move || { + counter.borrow_mut().effect += 1; + println!("Signal: {:?}", signal); + }) + }); + signal += 1; + + render! { + div {} + } + }, + counter.clone(), + ); + + let _ = dom.rebuild().santize(); + + let current_counter = counter.borrow(); + assert_eq!(current_counter.component, 1); + assert_eq!(current_counter.effect, 2); +} diff --git a/packages/signals/tests/selector.rs b/packages/signals/tests/selector.rs new file mode 100644 index 000000000..17eb1c38a --- /dev/null +++ b/packages/signals/tests/selector.rs @@ -0,0 +1,145 @@ +#![allow(unused, non_upper_case_globals, non_snake_case)] +use std::collections::HashMap; +use std::rc::Rc; + +use dioxus::html::p; +use dioxus::prelude::*; +use dioxus_core::ElementId; +use dioxus_signals::*; + +#[test] +fn memos_rerun() { + let _ = simple_logger::SimpleLogger::new().init(); + + #[derive(Default)] + struct RunCounter { + component: usize, + effect: usize, + } + + let counter = Rc::new(RefCell::new(RunCounter::default())); + let mut dom = VirtualDom::new_with_props( + |cx| { + let counter = cx.props; + counter.borrow_mut().component += 1; + + let mut signal = use_signal(cx, || 0); + let memo = cx.use_hook(move || { + to_owned![counter]; + selector(move || { + counter.borrow_mut().effect += 1; + println!("Signal: {:?}", signal); + signal.value() + }) + }); + assert_eq!(memo.value(), 0); + signal += 1; + assert_eq!(memo.value(), 1); + + render! { + div {} + } + }, + counter.clone(), + ); + + let _ = dom.rebuild().santize(); + + let current_counter = counter.borrow(); + assert_eq!(current_counter.component, 1); + assert_eq!(current_counter.effect, 2); +} + +#[test] +fn memos_prevents_component_rerun() { + let _ = simple_logger::SimpleLogger::new().init(); + + #[derive(Default)] + struct RunCounter { + component: usize, + effect: usize, + } + + let counter = Rc::new(RefCell::new(RunCounter::default())); + let mut dom = VirtualDom::new_with_props( + |cx| { + let mut signal = use_signal(cx, || 0); + + if cx.generation() == 1 { + *signal.write() = 0; + } + if cx.generation() == 2 { + println!("Writing to signal"); + *signal.write() = 1; + } + + render! { + Child { + signal: signal, + counter: cx.props.clone(), + } + } + }, + counter.clone(), + ); + + #[derive(Default, Props)] + struct ChildProps { + signal: Signal, + counter: Rc>, + } + + impl PartialEq for ChildProps { + fn eq(&self, other: &Self) -> bool { + self.signal == other.signal + } + } + + fn Child(cx: Scope) -> Element { + let counter = &cx.props.counter; + let signal = cx.props.signal; + counter.borrow_mut().component += 1; + + let memo = cx.use_hook(move || { + to_owned![counter]; + selector(move || { + counter.borrow_mut().effect += 1; + println!("Signal: {:?}", signal); + signal.value() + }) + }); + match cx.generation() { + 0 => { + assert_eq!(memo.value(), 0); + } + 1 => { + assert_eq!(memo.value(), 1); + } + _ => panic!("Unexpected generation"), + } + + render! { + div {} + } + } + + let _ = dom.rebuild().santize(); + dom.mark_dirty(ScopeId(0)); + dom.render_immediate(); + + { + let current_counter = counter.borrow(); + assert_eq!(current_counter.component, 1); + assert_eq!(current_counter.effect, 2); + } + + dom.mark_dirty(ScopeId(0)); + dom.render_immediate(); + dom.render_immediate(); + + { + let current_counter = counter.borrow(); + assert_eq!(current_counter.component, 2); + assert_eq!(current_counter.effect, 3); + } +} diff --git a/packages/signals/tests/subscribe.rs b/packages/signals/tests/subscribe.rs new file mode 100644 index 000000000..5c1a99347 --- /dev/null +++ b/packages/signals/tests/subscribe.rs @@ -0,0 +1,92 @@ +#![allow(unused, non_upper_case_globals, non_snake_case)] +use std::collections::HashMap; +use std::rc::Rc; + +use dioxus::prelude::*; +use dioxus_core::ElementId; +use dioxus_signals::*; + +#[test] +fn reading_subscribes() { + simple_logger::SimpleLogger::new().init().unwrap(); + + #[derive(Default)] + struct RunCounter { + parent: usize, + children: HashMap, + } + + let counter = Rc::new(RefCell::new(RunCounter::default())); + let mut dom = VirtualDom::new_with_props( + |cx| { + let mut signal = use_signal(cx, || 0); + + println!("Parent: {:?}", cx.scope_id()); + if cx.generation() == 1 { + signal += 1; + } + + cx.props.borrow_mut().parent += 1; + + render! { + for id in 0..10 { + Child { + signal: signal, + counter: cx.props.clone() + } + } + } + }, + counter.clone(), + ); + + #[derive(Props, Clone)] + struct ChildProps { + signal: Signal, + counter: Rc>, + } + + impl PartialEq for ChildProps { + fn eq(&self, other: &Self) -> bool { + self.signal == other.signal + } + } + + fn Child(cx: Scope) -> Element { + println!("Child: {:?}", cx.scope_id()); + *cx.props + .counter + .borrow_mut() + .children + .entry(cx.scope_id()) + .or_default() += 1; + + render! { + "{cx.props.signal}" + } + } + + let _ = dom.rebuild().santize(); + + { + let current_counter = counter.borrow(); + assert_eq!(current_counter.parent, 1); + + for (scope_id, rerun_count) in current_counter.children.iter() { + assert_eq!(rerun_count, &1); + } + } + + dom.mark_dirty(ScopeId(0)); + dom.render_immediate(); + dom.render_immediate(); + + { + let current_counter = counter.borrow(); + assert_eq!(current_counter.parent, 2); + + for (scope_id, rerun_count) in current_counter.children.iter() { + assert_eq!(rerun_count, &2); + } + } +}