From 04cdb14e5bbe624a4f083ec9d6a805677f1c7634 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 13:53:05 -0700 Subject: [PATCH 01/85] improve signal copy runtime --- packages/core/src/arena.rs | 7 +- packages/core/src/create.rs | 6 +- packages/core/src/diff.rs | 37 +++- packages/core/src/lib.rs | 11 +- packages/core/src/runtime.rs | 87 ++++++++ packages/core/src/scheduler/wait.rs | 6 +- packages/core/src/scope_arena.rs | 47 ++--- packages/core/src/scope_context.rs | 288 ++++++++++++++++++++++++++ packages/core/src/scopes.rs | 131 +++--------- packages/core/src/virtual_dom.rs | 36 ++-- packages/signals/Cargo.toml | 1 + packages/signals/src/copy.rs | 301 ++++++++++++++++++++++++++++ packages/signals/src/lib.rs | 99 ++++----- packages/signals/src/rt.rs | 140 ++++--------- 14 files changed, 878 insertions(+), 319 deletions(-) create mode 100644 packages/core/src/runtime.rs create mode 100644 packages/core/src/scope_context.rs create mode 100644 packages/signals/src/copy.rs diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index b6254136d..25b040643 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -91,7 +91,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 +110,11 @@ 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); + 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..f332760a5 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -66,9 +66,9 @@ 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); + self.runtime.scope_stack.borrow_mut().push(scope); let out = self.create(template); - self.scope_stack.pop(); + self.runtime.scope_stack.borrow_mut().pop(); out } @@ -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..da1716e70 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -15,9 +15,8 @@ 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); + self.runtime.scope_stack.borrow_mut().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 +46,7 @@ impl<'b> VirtualDom { (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), }; } - self.scope_stack.pop(); + self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { @@ -210,7 +209,7 @@ impl<'b> VirtualDom { self.diff_scope(scope_id); self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[scope_id.0].height, + height: self.runtime.scope_contexts[scope_id.0].height, id: scope_id, }); } @@ -714,7 +713,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 +919,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 +945,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 +966,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/lib.rs b/packages/core/src/lib.rs index 565ae12be..b65eaa86a 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::*; @@ -81,9 +84,11 @@ 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, current_scope_id, fc_to_builder, has_context, provide_context, + provide_root_context, schedule_update_any, 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..7a9921e99 --- /dev/null +++ b/packages/core/src/runtime.rs @@ -0,0 +1,87 @@ +use std::cell::RefCell; + +use crate::{ + innerlude::{DirtyScope, Scheduler, SchedulerMsg}, + scope_context::ScopeContext, + scopes::{ScopeId, ScopeState}, +}; +use rustc_hash::FxHashSet; +use slab::Slab; +use std::{collections::BTreeSet, rc::Rc}; + +thread_local! { + static RUNTIMES: RefCell> = RefCell::new(vec![]); +} + +/// Pushes a new scope onto the stack +pub(crate) fn push_runtime(runtime: Runtime) { + 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(f) + }) +} + +/// 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(f)) + }) + .flatten() +} + +pub struct Runtime { + pub(crate) scope_contexts: Slab, + pub(crate) scheduler: Rc, + + // While diffing we need some sort of way of breaking off a stream of suspended mutations. + pub(crate) scope_stack: RefCell>, +} + +impl Runtime { + pub(crate) fn new(scheduler: Rc) -> Rc { + Rc::new(Self { + scheduler, + + scope_contexts: Default::default(), + + scope_stack: Default::default(), + }) + } + + /// Get the current scope id + pub fn current_scope_id(&self) -> Option { + self.scope_stack.borrow().last().copied() + } + + /// Get the state 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<&ScopeContext> { + self.scope_contexts.get(id.0).map(|f| &*f) + } + + /// Get the single scope at the top of the Runtime tree that will always be around + /// + /// This scope has a ScopeId of 0 and is the root of the tree + pub fn base_context(&self) -> &ScopeContext { + self.get_context(ScopeId(0)).unwrap() + } +} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index b475c6751..a04bf2730 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -7,7 +7,7 @@ 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 mut tasks = self.runtime.scheduler.tasks.borrow_mut(); let task = match tasks.get(id.0) { Some(task) => task, @@ -20,8 +20,8 @@ impl VirtualDom { // 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); diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index b298a8996..1a64612bf 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -13,35 +13,29 @@ 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, + entry.insert(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()) + }) } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { @@ -51,10 +45,10 @@ impl VirtualDom { 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,18 +71,19 @@ 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 diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs new file mode 100644 index 000000000..e1a64bc2b --- /dev/null +++ b/packages/core/src/scope_context.rs @@ -0,0 +1,288 @@ +use crate::{ + innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, + runtime::{with_current_scope, with_runtime}, + Element, ScopeId, TaskId, +}; +use rustc_hash::FxHashSet; +use std::{ + any::{Any, TypeId}, + 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 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 fn name(&self) -> &str { + self.name + } + + pub fn height(&self) -> u32 { + self.height + } + + 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(|(k, _)| *k == TypeId::of::()) + .map(|(_, v)| v)? + .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| { + let mut result = None; + 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(|(k, _)| *k == TypeId::of::()) + { + result = shared.1.downcast_ref::().cloned(); + break; + } + search_parent = parent.parent_id; + } + result + }) + .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.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 + } + + /// 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| { + // Walk upwards until there is no more parent - and tada we have the root + let mut parent = runtime.get_context(self.scope_id()).unwrap(); + while let Some(next_parent) = parent.parent_id { + parent = runtime.get_context(next_parent).unwrap(); + } + debug_assert_eq!(parent.scope_id(), ScopeId(0)); + + parent.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 [`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() +} + +/// Consume context from the current scope +pub fn consume_context() -> Option { + with_current_scope(|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 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() +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 9470c3d0d..d1897331a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -2,16 +2,17 @@ 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}, + any::Any, cell::{Cell, RefCell, UnsafeCell}, fmt::{Arguments, Debug}, future::Future, @@ -66,26 +67,17 @@ 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>>, @@ -93,6 +85,10 @@ pub struct ScopeState { } impl<'src> ScopeState { + pub(crate) fn context(&self) -> &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 +107,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 +170,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`]. @@ -193,9 +189,9 @@ impl<'src> ScopeState { /// /// assert_eq!(base.parent(), None); /// ``` - pub fn parent(&self) -> Option { + pub fn parent_id(&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 +208,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 +224,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 +273,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 +284,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. @@ -481,7 +411,7 @@ impl<'src> ScopeState { /// fn(Scope) -> Element; /// async fn(Scope>) -> Element; /// ``` - pub fn component

( + pub fn component( &'src self, component: fn(Scope<'src, P>) -> Element<'src>, props: P, @@ -564,8 +494,7 @@ impl<'src> ScopeState { /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { - self.suspended.set(true); - None + self.context().suspend() } /// Store a value between renders. The foundational hook for all other hooks. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index dce5415f6..0d22cbeea 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, 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) } /// 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 }); } } @@ -501,10 +502,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, }); } } diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index a0c3534b0..bfa16363b 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -9,3 +9,4 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } slab = { workspace = true } +bumpalo = { version = "3.6" } diff --git a/packages/signals/src/copy.rs b/packages/signals/src/copy.rs new file mode 100644 index 000000000..a10540429 --- /dev/null +++ b/packages/signals/src/copy.rs @@ -0,0 +1,301 @@ +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + 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(debug_assertions)] + 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()); + } +} + +pub struct CopyHandle { + raw: MemoryLocation, + #[cfg(debug_assertions)] + generation: u32, + _marker: PhantomData, +} + +impl CopyHandle { + #[inline(always)] + fn validate(&self) -> bool { + #[cfg(debug_assertions)] + { + self.raw.generation.get() == self.generation + } + #[cfg(not(debug_assertions))] + { + true + } + } + + pub fn try_read(&self) -> Option> { + self.validate() + .then(|| { + Ref::filter_map(self.raw.data.borrow(), |any| { + any.as_ref()?.downcast_ref::() + }) + .ok() + }) + .flatten() + } + + pub fn read(&self) -> Ref<'_, T> { + self.try_read().unwrap() + } + + 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() + } + + pub fn write(&self) -> RefMut<'_, T> { + self.try_write().unwrap() + } +} + +impl Copy for CopyHandle {} + +impl Clone for CopyHandle { + fn clone(&self) -> Self { + *self + } +} + +#[derive(Clone, Copy)] +struct MemoryLocation { + data: &'static RefCell>>, + #[cfg(debug_assertions)] + generation: &'static Cell, +} + +impl MemoryLocation { + #[allow(unused)] + fn drop(&self) { + let old = self.data.borrow_mut().take(); + #[cfg(debug_assertions)] + if old.is_some() { + let new_generation = self.generation.get() + 1; + self.generation.set(new_generation); + } + } + + fn replace(&mut self, value: T) -> CopyHandle { + let mut inner_mut = self.data.borrow_mut(); + + let raw = Box::new(value); + let old = inner_mut.replace(raw); + assert!(old.is_none()); + CopyHandle { + raw: *self, + #[cfg(debug_assertions)] + generation: self.generation.get(), + _marker: PhantomData, + } + } +} + +#[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(debug_assertions)] + generation: self.bump.alloc(Cell::new(0)), + } + } + } + + pub fn owner(&self) -> Owner { + Owner { + store: self.clone(), + owned: Default::default(), + } + } +} + +pub struct Owner { + store: Store, + owned: Rc>>, +} + +impl Owner { + pub fn insert(&self, value: T) -> CopyHandle { + let mut location = self.store.claim(); + let key = location.replace(value); + self.owned.borrow_mut().push(location); + key + } +} + +impl Drop for Owner { + fn drop(&mut self) { + for location in self.owned.borrow().iter() { + self.store.recycle(*location) + } + } +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 5b6a740cf..803d07a61 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,70 +1,65 @@ use std::{ cell::{Ref, RefMut}, fmt::Display, - marker::PhantomData, ops::{Add, Div, Mul, Sub}, + sync::Arc, }; +mod copy; 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); - }); -} +use dioxus_core::{ + prelude::{current_scope_id, schedule_update_any}, + ScopeId, ScopeState, +}; 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 + *cx.use_hook(|| Signal::new(f())) } -pub struct Signal { - id: usize, - rt: &'static SignalRt, - t: PhantomData, +struct SignalData { + subscribers: Vec, + update_any: Arc, + value: T, +} + +pub struct Signal { + inner: CopyValue>, } impl Signal { + pub fn new(value: T) -> Self { + Self { + inner: CopyValue::new(SignalData { + subscribers: Vec::new(), + update_any: schedule_update_any().expect("in a virtual dom"), + value, + }), + } + } + pub fn read(&self) -> Ref { - self.rt.read(self.id) + if let Some(current_scope_id) = current_scope_id() { + let mut inner = self.inner.write(); + if !inner.subscribers.contains(¤t_scope_id) { + inner.subscribers.push(current_scope_id); + } + } + Ref::map(self.inner.read(), |v| &v.value) } pub fn write(&self) -> RefMut { - self.rt.write(self.id) + let inner = self.inner.write(); + for &scope_id in &inner.subscribers { + (inner.update_any)(scope_id); + } + + RefMut::map(inner, |v| &mut v.value) } pub fn set(&mut self, value: T) { - self.rt.set(self.id, value); + *self.write() = value; } pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { @@ -72,23 +67,15 @@ impl Signal { f(&*write) } - pub fn update(&self, _f: impl FnOnce(&mut T) -> O) -> O { + pub fn update(&self, f: impl FnOnce(&mut T) -> O) -> O { let mut write = self.write(); - _f(&mut *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) + self.read().clone() } } @@ -102,7 +89,7 @@ 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)) + self.with(|v| Display::fmt(v, f)) } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index e849f9008..def6666b1 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -1,121 +1,65 @@ -use std::{any::Any, cell::RefCell, sync::Arc}; +use std::cell::{Ref, RefMut}; -use dioxus_core::ScopeId; -use slab::Slab; +use std::rc::Rc; -thread_local! { - // we cannot drop these since any future might be using them - static RUNTIMES: RefCell> = RefCell::new(Vec::new()); -} +use dioxus_core::prelude::{consume_context, provide_root_context}; -/// Provide the runtime for signals -/// -/// 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; +use crate::copy::{CopyHandle, 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") } - - Box::leak(Box::new(SignalRt { - signals: RefCell::new(Slab::new()), - update_any, - })) - }) + } } -/// Push this runtime into the global runtime list -pub fn reclam_rt(_rt: &'static SignalRt) { - RUNTIMES.with(|runtimes| { - runtimes.borrow_mut().push(_rt); - }); +fn current_owner() -> Rc { + match consume_context() { + Some(rt) => rt, + None => { + let owner = Rc::new(current_store().owner()); + provide_root_context(owner).expect("in a virtual dom") + } + } } -pub struct SignalRt { - pub(crate) signals: RefCell>, - pub(crate) update_any: Arc, +impl Copy for CopyValue {} + +impl Clone for CopyValue { + fn clone(&self) -> Self { + *self + } } -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, - }) - } +pub struct CopyValue { + pub value: CopyHandle, +} - pub fn subscribe(&self, id: usize, subscriber: ScopeId) { - self.signals.borrow_mut()[id].subscribers.push(subscriber); - } +impl CopyValue { + pub fn new(value: T) -> Self { + let owner = current_owner(); - 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), } } - pub fn remove(&self, id: usize) { - self.signals.borrow_mut().remove(id); + pub fn try_read(&self) -> Option> { + self.value.try_read() } - 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 fn read(&self) -> Ref<'_, T> { + self.value.read() } - 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 fn try_write(&self) -> Option> { + self.value.try_write() } - 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); - } - - let r = inner.getter.as_ref().unwrap(); - - unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) } + pub fn write(&self) -> RefMut<'_, T> { + self.value.write() } } - -pub(crate) struct Inner { - pub value: Box, - pub subscribers: Vec, - - // todo: this has a soundness hole in it that you might not run into - pub getter: Option>, -} From cb4c46154d6b145c9fdb98e34d1e43bd2c211437 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:12:59 -0700 Subject: [PATCH 02/85] fix scope context --- examples/clock.rs | 4 +- packages/core/src/arena.rs | 10 +++-- packages/core/src/diff.rs | 2 +- packages/core/src/runtime.rs | 64 ++++++++++++++++++------------ packages/core/src/scope_arena.rs | 11 ++++- packages/core/src/scope_context.rs | 23 ++++++++--- packages/core/src/scopes.rs | 14 +++++-- 7 files changed, 83 insertions(+), 45 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index 505a5e87c..e59538edc 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -3,15 +3,13 @@ //! 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/packages/core/src/arena.rs b/packages/core/src/arena.rs index 25b040643..30a9cba8f 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -110,11 +110,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(); + { + let context = scope.context(); - // Drop all the futures once the hooks are dropped - for task_id in context.spawned_tasks.borrow_mut().drain() { - context.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/diff.rs b/packages/core/src/diff.rs index da1716e70..e3ca19c3a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -209,7 +209,7 @@ impl<'b> VirtualDom { self.diff_scope(scope_id); self.dirty_scopes.remove(&DirtyScope { - height: self.runtime.scope_contexts[scope_id.0].height, + height: self.runtime.get_context(scope_id).unwrap().height, id: scope_id, }); } diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 7a9921e99..7b9c7c05c 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -1,20 +1,14 @@ -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; -use crate::{ - innerlude::{DirtyScope, Scheduler, SchedulerMsg}, - scope_context::ScopeContext, - scopes::{ScopeId, ScopeState}, -}; -use rustc_hash::FxHashSet; -use slab::Slab; -use std::{collections::BTreeSet, rc::Rc}; +use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId}; +use std::rc::Rc; thread_local! { - static RUNTIMES: RefCell> = RefCell::new(vec![]); + static RUNTIMES: RefCell>> = RefCell::new(vec![]); } /// Pushes a new scope onto the stack -pub(crate) fn push_runtime(runtime: Runtime) { +pub(crate) fn push_runtime(runtime: Rc) { RUNTIMES.with(|stack| stack.borrow_mut().push(runtime)); } @@ -30,7 +24,7 @@ where { RUNTIMES.with(|stack| { let stack = stack.borrow(); - stack.last().map(f) + stack.last().map(|r| f(&**r)) }) } @@ -42,28 +36,50 @@ where with_runtime(|runtime| { runtime .current_scope_id() - .and_then(|scope| runtime.get_context(scope).map(f)) + .and_then(|scope| runtime.get_context(scope).map(|sc| f(&*sc))) }) .flatten() } pub struct Runtime { - pub(crate) scope_contexts: Slab, + pub(crate) scope_contexts: RefCell>>, pub(crate) scheduler: Rc, // While diffing we need some sort of way of breaking off a stream of suspended mutations. pub(crate) scope_stack: RefCell>, } +impl Drop for Runtime { + fn drop(&mut self) { + // todo: do this better + pop_runtime(); + } +} + impl Runtime { pub(crate) fn new(scheduler: Rc) -> Rc { - Rc::new(Self { + let runtime = Rc::new(Self { scheduler, scope_contexts: Default::default(), scope_stack: Default::default(), - }) + }); + push_runtime(runtime.clone()); + runtime + } + + /// 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 @@ -71,17 +87,13 @@ impl Runtime { self.scope_stack.borrow().last().copied() } - /// Get the state for any scope given its ID + /// 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<&ScopeContext> { - self.scope_contexts.get(id.0).map(|f| &*f) - } - - /// Get the single scope at the top of the Runtime tree that will always be around - /// - /// This scope has a ScopeId of 0 and is the root of the tree - pub fn base_context(&self) -> &ScopeContext { - self.get_context(ScopeId(0)).unwrap() + 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() } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 1a64612bf..2d5e09b0b 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, }; @@ -20,7 +21,7 @@ impl VirtualDom { let entry = self.scopes.vacant_entry(); let id = ScopeId(entry.key()); - entry.insert(ScopeState { + let scope = entry.insert(ScopeState { runtime: self.runtime.clone(), context_id: id, @@ -35,7 +36,13 @@ impl VirtualDom { borrowed_props: Default::default(), attributes_to_drop: Default::default(), - }) + }); + + 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 { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index e1a64bc2b..340845afc 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -32,12 +32,23 @@ pub struct ScopeContext { } impl ScopeContext { - pub fn name(&self) -> &str { - self.name - } - - pub fn height(&self) -> u32 { - self.height + 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 { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index d1897331a..e17e08aa9 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -13,7 +13,7 @@ use crate::{ use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ any::Any, - cell::{Cell, RefCell, UnsafeCell}, + cell::{Cell, Ref, RefCell, UnsafeCell}, fmt::{Arguments, Debug}, future::Future, rc::Rc, @@ -84,8 +84,14 @@ pub struct ScopeState { 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) -> &ScopeContext { + pub(crate) fn context(&self) -> Ref<'_, ScopeContext> { self.runtime.get_context(self.context_id).unwrap() } @@ -494,7 +500,9 @@ impl<'src> ScopeState { /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { - self.context().suspend() + let cx = self.context(); + cx.suspend(); + None } /// Store a value between renders. The foundational hook for all other hooks. From b09e528aaab2f18274d66d6bc158009c68253fee Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:14:56 -0700 Subject: [PATCH 03/85] fix recursive runtime drop impl --- packages/core/src/runtime.rs | 8 -------- packages/core/src/virtual_dom.rs | 7 ++++++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 7b9c7c05c..839a055df 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -49,13 +49,6 @@ pub struct Runtime { pub(crate) scope_stack: RefCell>, } -impl Drop for Runtime { - fn drop(&mut self) { - // todo: do this better - pop_runtime(); - } -} - impl Runtime { pub(crate) fn new(scheduler: Rc) -> Rc { let runtime = Rc::new(Self { @@ -65,7 +58,6 @@ impl Runtime { scope_stack: Default::default(), }); - push_runtime(runtime.clone()); runtime } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 0d22cbeea..1a2cf5c5d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,7 +9,7 @@ use crate::{ mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, - runtime::Runtime, + runtime::{pop_runtime, push_runtime, Runtime}, scopes::{ScopeId, ScopeState}, AttributeValue, Element, Event, Scope, }; @@ -275,6 +275,9 @@ impl VirtualDom { // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(ElementRef::none()); + // Set this as the current runtime + push_runtime(dom.runtime.clone()); + dom } @@ -648,5 +651,7 @@ impl Drop for VirtualDom { fn drop(&mut self) { // Simply drop this scope which drops all of its children self.drop_scope(ScopeId(0), true); + // remove the current runtime + pop_runtime(); } } From a2c7b69a1dc617b5e9f064761d1fe41ad535c621 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:19:59 -0700 Subject: [PATCH 04/85] set the scope stack correctly --- packages/core/src/create.rs | 7 ++----- packages/core/src/diff.rs | 2 -- packages/core/src/runtime.rs | 2 +- packages/core/src/scope_arena.rs | 7 ++++++- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index f332760a5..17f3229d6 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -65,11 +65,8 @@ impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// /// 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.runtime.scope_stack.borrow_mut().push(scope); - let out = self.create(template); - self.runtime.scope_stack.borrow_mut().pop(); - out + pub(crate) fn create_scope(&mut self, _scope: ScopeId, template: &'b VNode<'b>) -> usize { + self.create(template) } /// Create this template and write its mutations diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e3ca19c3a..270c81730 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -15,7 +15,6 @@ use DynamicNode::*; impl<'b> VirtualDom { pub(super) fn diff_scope(&mut self, scope: ScopeId) { - self.runtime.scope_stack.borrow_mut().push(scope); let scope_state = &mut self.get_scope(scope).unwrap(); unsafe { // Load the old and new bump arenas @@ -46,7 +45,6 @@ impl<'b> VirtualDom { (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), }; } - self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 839a055df..1ca778edd 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -45,7 +45,7 @@ pub struct Runtime { pub(crate) scope_contexts: RefCell>>, pub(crate) scheduler: Rc, - // While diffing we need some sort of way of breaking off a stream of suspended mutations. + // We use this to track the current scope pub(crate) scope_stack: RefCell>, } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 2d5e09b0b..58fe97a90 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -46,6 +46,7 @@ impl VirtualDom { } 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 @@ -94,6 +95,10 @@ impl VirtualDom { } // 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 } } From 998f9e0d005cf9db6e6a14bfd56d3f5627956b00 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:24:49 -0700 Subject: [PATCH 05/85] remove static bound from props --- packages/core/src/scopes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index e17e08aa9..246d2801c 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -417,7 +417,7 @@ impl<'src> ScopeState { /// fn(Scope) -> Element; /// async fn(Scope>) -> Element; /// ``` - pub fn component( + pub fn component

( &'src self, component: fn(Scope<'src, P>) -> Element<'src>, props: P, From 2b6b3b67b1f8d9c8a0bcba333e9bc62b0578a001 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:28:55 -0700 Subject: [PATCH 06/85] move dioxus copy into a separate package --- Cargo.toml | 2 ++ packages/copy/Cargo.toml | 10 ++++++++++ packages/{signals/src/copy.rs => copy/src/lib.rs} | 0 packages/signals/Cargo.toml | 3 +-- packages/signals/src/lib.rs | 1 - packages/signals/src/rt.rs | 2 +- 6 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 packages/copy/Cargo.toml rename packages/{signals/src/copy.rs => copy/src/lib.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index e6ce4459b..a544335f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", + "packages/copy", "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" } +dioxus-copy = { path = "packages/copy" } 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/packages/copy/Cargo.toml b/packages/copy/Cargo.toml new file mode 100644 index 000000000..acb36426e --- /dev/null +++ b/packages/copy/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dioxus-copy" +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" } diff --git a/packages/signals/src/copy.rs b/packages/copy/src/lib.rs similarity index 100% rename from packages/signals/src/copy.rs rename to packages/copy/src/lib.rs diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index bfa16363b..1210442aa 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -8,5 +8,4 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } -slab = { workspace = true } -bumpalo = { version = "3.6" } +dioxus-copy = { workspace = true } \ No newline at end of file diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 803d07a61..2a30d0798 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -5,7 +5,6 @@ use std::{ sync::Arc, }; -mod copy; mod rt; pub use rt::*; diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index def6666b1..a47b5764b 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use dioxus_core::prelude::{consume_context, provide_root_context}; -use crate::copy::{CopyHandle, Owner, Store}; +use dioxus_copy::{CopyHandle, Owner, Store}; fn current_store() -> Store { match consume_context() { From 0be5dbe71508bf33903ddf10258f933db6f7183b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:42:19 -0700 Subject: [PATCH 07/85] fix the context API --- packages/core/src/create.rs | 7 +++++-- packages/core/src/scope_context.rs | 8 +++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 17f3229d6..7fbc8a9c8 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -65,8 +65,11 @@ impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// /// 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.create(template) + pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize { + 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 diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 340845afc..13263f529 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -115,21 +115,19 @@ impl ScopeContext { let mut search_parent = self.parent_id; with_runtime(|runtime| { - let mut result = None; 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(|(k, _)| *k == TypeId::of::()) + .find_map(|(_, any)| any.downcast_ref::()) { - result = shared.1.downcast_ref::().cloned(); - break; + return Some(shared.clone()); } search_parent = parent.parent_id; } - result + None }) .flatten() } From 3242fc9e88c741e33a440d326d03cf59fa6b5d29 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:49:52 -0700 Subject: [PATCH 08/85] add debug impl --- packages/signals/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 2a30d0798..cfb25c96a 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,6 +1,6 @@ use std::{ cell::{Ref, RefMut}, - fmt::Display, + fmt::{Debug, Display}, ops::{Add, Div, Mul, Sub}, sync::Arc, }; @@ -92,6 +92,12 @@ impl Display for Signal { } } +impl Debug for Signal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.with(|v| Debug::fmt(v, f)) + } +} + impl + Copy + 'static> std::ops::AddAssign for Signal { fn add_assign(&mut self, rhs: T) { self.set(self.get() + rhs); From 48b1e9e54cae4a09c2f069cba4e4946c8d9d624f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 14:53:34 -0700 Subject: [PATCH 09/85] fix copy dependencies --- packages/copy/Cargo.toml | 3 +++ packages/signals/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/copy/Cargo.toml b/packages/copy/Cargo.toml index acb36426e..6e623f67f 100644 --- a/packages/copy/Cargo.toml +++ b/packages/copy/Cargo.toml @@ -8,3 +8,6 @@ edition = "2018" [dependencies] bumpalo = { version = "3.6" } + +[dev-dependencies] +rand = "0.8.5" diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 1210442aa..11d5542d7 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } -dioxus-copy = { workspace = true } \ No newline at end of file +dioxus-copy = { workspace = true } From 0032f7e2af4b7335f25ad71c09142363188e0dd1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 15:23:09 -0700 Subject: [PATCH 10/85] fix miri --- packages/core/src/scope_arena.rs | 4 ++-- packages/core/src/virtual_dom.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 58fe97a90..138431740 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -21,7 +21,7 @@ impl VirtualDom { let entry = self.scopes.vacant_entry(); let id = ScopeId(entry.key()); - let scope = entry.insert(ScopeState { + let scope = entry.insert(Box::new(ScopeState { runtime: self.runtime.clone(), context_id: id, @@ -36,7 +36,7 @@ impl VirtualDom { borrowed_props: Default::default(), attributes_to_drop: Default::default(), - }); + })); let context = ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone()); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1a2cf5c5d..86094da9d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -175,7 +175,7 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// } /// ``` pub struct VirtualDom { - pub(crate) scopes: Slab, + pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, @@ -285,7 +285,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) + self.scopes.get(id.0).map(|s| &**s) } /// Get the single scope at the top of the VirtualDom tree that will always be around From 07ed3e67dd89cb1e7fb99f985e56197db843b4db Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 15:46:04 -0700 Subject: [PATCH 11/85] add check_generation feature flag --- packages/copy/Cargo.toml | 4 ++++ packages/copy/src/lib.rs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/copy/Cargo.toml b/packages/copy/Cargo.toml index 6e623f67f..9c5e84065 100644 --- a/packages/copy/Cargo.toml +++ b/packages/copy/Cargo.toml @@ -11,3 +11,7 @@ bumpalo = { version = "3.6" } [dev-dependencies] rand = "0.8.5" + +[features] +default = ["check_generation"] +check_generation = [] \ No newline at end of file diff --git a/packages/copy/src/lib.rs b/packages/copy/src/lib.rs index a10540429..8e8b8053f 100644 --- a/packages/copy/src/lib.rs +++ b/packages/copy/src/lib.rs @@ -128,7 +128,7 @@ fn fuzz() { println!("{:?}", value); assert!(value.starts_with("hello world")); } - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] for key in invalid_keys.iter() { assert!(!key.validate()); } @@ -146,7 +146,7 @@ fn fuzz() { pub struct CopyHandle { raw: MemoryLocation, - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] generation: u32, _marker: PhantomData, } @@ -154,11 +154,11 @@ pub struct CopyHandle { impl CopyHandle { #[inline(always)] fn validate(&self) -> bool { - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] { self.raw.generation.get() == self.generation } - #[cfg(not(debug_assertions))] + #[cfg(not(any(debug_assertions, feature = "check_generation")))] { true } @@ -206,7 +206,7 @@ impl Clone for CopyHandle { #[derive(Clone, Copy)] struct MemoryLocation { data: &'static RefCell>>, - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] generation: &'static Cell, } @@ -214,7 +214,7 @@ impl MemoryLocation { #[allow(unused)] fn drop(&self) { let old = self.data.borrow_mut().take(); - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] if old.is_some() { let new_generation = self.generation.get() + 1; self.generation.set(new_generation); @@ -229,7 +229,7 @@ impl MemoryLocation { assert!(old.is_none()); CopyHandle { raw: *self, - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] generation: self.generation.get(), _marker: PhantomData, } @@ -264,7 +264,7 @@ impl Store { let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None)); MemoryLocation { data, - #[cfg(debug_assertions)] + #[cfg(any(debug_assertions, feature = "check_generation"))] generation: self.bump.alloc(Cell::new(0)), } } From a6f611eccf2332c40be22ba052a8d30bab6b13c8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 17:23:57 -0700 Subject: [PATCH 12/85] fix subscriptions in events --- examples/clock.rs | 62 +++++++-- packages/copy/Cargo.toml | 2 +- packages/copy/src/lib.rs | 11 ++ packages/core/src/arena.rs | 2 +- packages/core/src/events.rs | 6 +- packages/core/src/nodes.rs | 6 +- packages/core/src/scopes.rs | 26 ++-- packages/core/src/virtual_dom.rs | 10 +- packages/signals/src/effect.rs | 52 ++++++++ packages/signals/src/impls.rs | 212 +++++++++++++++++++++++++++++++ packages/signals/src/lib.rs | 125 ++++++++++-------- packages/signals/src/rt.rs | 34 +++-- 12 files changed, 445 insertions(+), 103 deletions(-) create mode 100644 packages/signals/src/effect.rs create mode 100644 packages/signals/src/impls.rs diff --git a/examples/clock.rs b/examples/clock.rs index e59538edc..b0ea29b40 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,26 +1,60 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. +#![allow(non_snake_case)] use dioxus::prelude::*; -use dioxus_signals::use_signal; +use dioxus_signals::{use_signal, Effect, Signal}; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let mut count = use_signal(cx, || 0); + let counts = use_signal(cx, || (0..100).map(Signal::new).collect::>()); - use_future!(cx, || async move { - loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - count += 1; - println!("current: {count}"); - } + cx.use_hook(|| { + Effect::new(move || { + println!("Counts: {:?}", counts); + }) }); - cx.render(rsx! { - div { "High-Five counter: {count}" } - }) + render! { + for count in counts { + Child { + count: count, + } + } + } +} + +#[derive(Props, PartialEq)] +struct ChildProps { + count: Signal, +} + +fn Child(cx: Scope) -> Element { + let count = cx.props.count; + + // use_future!(cx, || async move { + // loop { + // tokio::time::sleep(std::time::Duration::from_secs(count.value())).await; + // *count.write() += 1; + // } + // }); + + render! { + div { + "Child: {count}" + button { + onclick: move |_| { + *count.write() += 1; + }, + "Increase" + } + button { + onclick: move |_| { + *count.write() -= 1; + }, + "Decrease" + } + } + } } diff --git a/packages/copy/Cargo.toml b/packages/copy/Cargo.toml index 9c5e84065..56964badd 100644 --- a/packages/copy/Cargo.toml +++ b/packages/copy/Cargo.toml @@ -14,4 +14,4 @@ rand = "0.8.5" [features] default = ["check_generation"] -check_generation = [] \ No newline at end of file +check_generation = [] diff --git a/packages/copy/src/lib.rs b/packages/copy/src/lib.rs index 8e8b8053f..3b091d30a 100644 --- a/packages/copy/src/lib.rs +++ b/packages/copy/src/lib.rs @@ -193,6 +193,17 @@ impl CopyHandle { pub fn write(&self) -> RefMut<'_, T> { self.try_write().unwrap() } + + 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 CopyHandle {} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 30a9cba8f..b7ec46623 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -163,7 +163,7 @@ impl VirtualDom { let listener = unsafe { &*listener }; match &listener.value { AttributeValue::Listener(l) => { - _ = l.take(); + _ = l.callback.take(); } AttributeValue::Any(a) => { _ = a.take(); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index ce958db40..c734ebd4a 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,7 +1,9 @@ use std::{ cell::{Cell, RefCell}, rc::Rc, + }; +use crate::ScopeId; /// A wrapper around some generic data that handles the event's state /// @@ -134,13 +136,15 @@ impl std::fmt::Debug for Event { /// } /// /// ``` -pub struct EventHandler<'bump, T = ()> { +pub struct EventHandler<'bump, T:?Sized = ()> { + pub(crate) origin: ScopeId, pub(super) callback: RefCell>>, } impl Default for EventHandler<'_, T> { fn default() -> Self { Self { + origin: ScopeId(0), callback: Default::default(), } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a108f3577..5406e8f07 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,4 @@ -use crate::{ +use crate::prelude::EventHandler;use crate::{ any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; use bumpalo::boxed::Box as BumpBox; @@ -476,7 +476,7 @@ pub enum AttributeValue<'a> { Bool(bool), /// A listener, like "onclick" - Listener(RefCell>>), + Listener(EventHandler<'a, Event>), /// An arbitrary value that implements PartialEq and is static Any(RefCell>>), @@ -485,8 +485,6 @@ pub enum AttributeValue<'a> { None, } -pub type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; - /// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements that are borrowed /// /// These varients are used to communicate what the value of an attribute is that needs to be updated diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 246d2801c..b43b2143a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -446,7 +446,7 @@ 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 @@ -456,22 +456,16 @@ impl<'src> ScopeState { &'src self, mut callback: impl FnMut(Event) + 'src, ) -> AttributeValue<'src> { - // safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw - // This is the suggested way to build a bumpbox - // - // In theory, we could just use regular boxes - let boxed: BumpBox<'src, dyn FnMut(_) + 'src> = unsafe { - BumpBox::from_raw(self.bump().alloc(move |event: Event| { - if let Ok(data) = event.data.downcast::() { - callback(Event { - propagates: event.propagates, - data, - }); - } - })) - }; + - AttributeValue::Listener(RefCell::new(Some(boxed))) + AttributeValue::Listener(self.event_handler(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propagates: event.propagates, + data, + }); + } + })) } /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 86094da9d..c43af2254 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -391,9 +391,12 @@ 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 { - if let Some(cb) = listener.borrow_mut().as_deref_mut() { + let origin = listener.origin; + self.runtime.scope_stack.borrow_mut().push(origin); + if let Some(cb) = listener.callback.borrow_mut().as_deref_mut() { cb(uievent.clone()); } + self.runtime.scope_stack.borrow_mut().pop(); if !uievent.propagates.get() { return; @@ -422,9 +425,12 @@ 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 { - if let Some(cb) = listener.borrow_mut().as_deref_mut() { + let origin = listener.origin; + self.runtime.scope_stack.borrow_mut().push(origin); + if let Some(cb) = listener.callback.borrow_mut().as_deref_mut() { cb(uievent.clone()); } + self.runtime.scope_stack.borrow_mut().pop(); break; } diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs new file mode 100644 index 000000000..9bc9b33f2 --- /dev/null +++ b/packages/signals/src/effect.rs @@ -0,0 +1,52 @@ +use dioxus_core::prelude::*; + +use crate::CopyValue; + +#[derive(Default, Clone, Copy)] +pub(crate) struct EffectStack { + pub(crate) effects: CopyValue>, +} + +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") + } + } +} + +#[derive(Copy, Clone, PartialEq)] +pub struct Effect { + callback: CopyValue>, +} + +impl Effect { + pub(crate) fn current() -> Option { + get_effect_stack().effects.read().last().copied() + } + + 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.write().push(*self); + } + callback(); + { + get_effect_stack().effects.write().pop(); + } + } + } +} diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs new file mode 100644 index 000000000..d54e5251f --- /dev/null +++ b/packages/signals/src/impls.rs @@ -0,0 +1,212 @@ +use crate::rt::CopyValue; +use crate::Signal; + +use std::cell::{Ref, RefMut}; + +use std::{ + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; + +macro_rules! impls { + ($ty:ident) => { + impl Default for $ty { + fn default() -> Self { + Self::new(T::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 + Copy + 'static> std::ops::AddAssign for $ty { + fn add_assign(&mut self, rhs: T) { + self.set(self.value() + rhs); + } + } + + impl + Copy + 'static> std::ops::SubAssign for $ty { + fn sub_assign(&mut self, rhs: T) { + self.set(self.value() - rhs); + } + } + + impl + Copy + 'static> std::ops::MulAssign for $ty { + fn mul_assign(&mut self, rhs: T) { + self.set(self.value() * rhs); + } + } + + impl + Copy + 'static> std::ops::DivAssign for $ty { + fn div_assign(&mut self, rhs: T) { + self.set(self.value() / rhs); + } + } + + impl $ty> { + pub fn push(&self, value: T) { + self.with_mut(|v| v.push(value)) + } + + pub fn pop(&self) -> Option { + self.with_mut(|v| v.pop()) + } + + pub fn insert(&self, index: usize, value: T) { + self.with_mut(|v| v.insert(index, value)) + } + + pub fn remove(&self, index: usize) -> T { + self.with_mut(|v| v.remove(index)) + } + + pub fn clear(&self) { + self.with_mut(|v| v.clear()) + } + + pub fn extend(&self, iter: impl IntoIterator) { + self.with_mut(|v| v.extend(iter)) + } + + pub fn truncate(&self, len: usize) { + self.with_mut(|v| v.truncate(len)) + } + + pub fn swap_remove(&self, index: usize) -> T { + self.with_mut(|v| v.swap_remove(index)) + } + + pub fn retain(&self, f: impl FnMut(&T) -> bool) { + self.with_mut(|v| v.retain(f)) + } + + pub fn split_off(&self, at: usize) -> Vec { + self.with_mut(|v| v.split_off(at)) + } + + pub fn get(&self, index: usize) -> Option> { + Ref::filter_map(self.read(), |v| v.get(index)).ok() + } + + pub fn get_mut(&self, index: usize) -> Option> { + RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() + } + } + + impl $ty> { + pub fn take(&self) -> Option { + self.with_mut(|v| v.take()) + } + + pub fn replace(&self, value: T) -> Option { + self.with_mut(|v| v.replace(value)) + } + + pub fn unwrap(&self) -> T + where + T: Clone, + { + self.with(|v| v.clone()).unwrap() + } + + pub fn as_ref(&self) -> Option> { + Ref::filter_map(self.read(), |v| v.as_ref()).ok() + } + + pub fn as_mut(&self) -> Option> { + RefMut::filter_map(self.write(), |v| v.as_mut()).ok() + } + + pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { + self.get_or_insert_with(|| default) + } + + 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()) + } + } + } + }; +} + +impls!(CopyValue); +impls!(Signal); + +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, + } + } +} + +pub struct CopySignalIterator { + index: usize, + value: Signal>, +} + +impl Iterator for CopySignalIterator { + 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 = CopySignalIterator; + + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + CopySignalIterator { + index: 0, + value: self, + } + } +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index cfb25c96a..4340b6484 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,15 +1,18 @@ use std::{ - cell::{Ref, RefMut}, - fmt::{Debug, Display}, - ops::{Add, Div, Mul, Sub}, + cell::{Ref, RefCell, RefMut}, + rc::Rc, sync::Arc, }; mod rt; pub use rt::*; +mod effect; +pub use effect::*; +#[macro_use] +mod impls; use dioxus_core::{ - prelude::{current_scope_id, schedule_update_any}, + prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, ScopeId, ScopeState, }; @@ -17,8 +20,36 @@ pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal< *cx.use_hook(|| Signal::new(f())) } +#[derive(Clone)] +struct Unsubscriber { + scope: ScopeId, + subscribers: 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") + } + } +} + struct SignalData { - subscribers: Vec, + subscribers: Rc>>, + effect_subscribers: Rc>>, update_any: Arc, value: T, } @@ -31,7 +62,8 @@ impl Signal { pub fn new(value: T) -> Self { Self { inner: CopyValue::new(SignalData { - subscribers: Vec::new(), + subscribers: Default::default(), + effect_subscribers: Default::default(), update_any: schedule_update_any().expect("in a virtual dom"), value, }), @@ -39,21 +71,40 @@ impl Signal { } pub fn read(&self) -> Ref { + let inner = self.inner.read(); if let Some(current_scope_id) = current_scope_id() { - let mut inner = self.inner.write(); - if !inner.subscribers.contains(¤t_scope_id) { - inner.subscribers.push(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(self.inner.read(), |v| &v.value) + if let Some(effect) = Effect::current() { + let mut effect_subscribers = inner.effect_subscribers.borrow_mut(); + if !effect_subscribers.contains(&effect) { + effect_subscribers.push(effect); + } + } + Ref::map(inner, |v| &v.value) } pub fn write(&self) -> RefMut { - let inner = self.inner.write(); - for &scope_id in &inner.subscribers { - (inner.update_any)(scope_id); + { + let inner = self.inner.read(); + for &scope_id in &*inner.subscribers.borrow() { + (inner.update_any)(scope_id); + } } + let subscribers = + { std::mem::take(&mut *self.inner.read().effect_subscribers.borrow_mut()) }; + for effect in subscribers { + effect.try_run(); + } + + let inner = self.inner.write(); RefMut::map(inner, |v| &mut v.value) } @@ -66,58 +117,20 @@ impl Signal { f(&*write) } - pub fn update(&self, f: impl FnOnce(&mut T) -> O) -> O { + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { let mut write = self.write(); f(&mut *write) } } impl Signal { - pub fn get(&self) -> T { + pub fn value(&self) -> T { self.read().clone() } } -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.with(|v| Display::fmt(v, f)) - } -} - -impl Debug for Signal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.with(|v| Debug::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); +impl PartialEq for Signal { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index a47b5764b..0605fb951 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -26,14 +26,6 @@ fn current_owner() -> Rc { } } -impl Copy for CopyValue {} - -impl Clone for CopyValue { - fn clone(&self) -> Self { - *self - } -} - pub struct CopyValue { pub value: CopyHandle, } @@ -62,4 +54,30 @@ impl CopyValue { pub fn write(&self) -> RefMut<'_, T> { self.value.write() } + + pub fn set(&mut self, value: T) { + *self.write() = value; + } + + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + f(&mut *write) + } +} + +impl CopyValue { + pub fn value(&self) -> T { + self.read().clone() + } +} + +impl PartialEq for CopyValue { + fn eq(&self, other: &Self) -> bool { + self.value.ptr_eq(&other.value) + } } From b3fbbba7113a3e53d49a171cca6a5f6ed72243b8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 17:25:40 -0700 Subject: [PATCH 13/85] fix signals in futures --- examples/clock.rs | 12 ++++++------ packages/core/src/scheduler/wait.rs | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index b0ea29b40..5c4bc4f25 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -33,12 +33,12 @@ struct ChildProps { fn Child(cx: Scope) -> Element { let count = cx.props.count; - // use_future!(cx, || async move { - // loop { - // tokio::time::sleep(std::time::Duration::from_secs(count.value())).await; - // *count.write() += 1; - // } - // }); + use_future!(cx, || async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(count.value())).await; + *count.write() += 1; + } + }); render! { div { diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index a04bf2730..47180c240 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -17,6 +17,9 @@ impl VirtualDom { let mut cx = Context::from_waker(&task.waker); + // update the scope stack + self.runtime.scope_stack.borrow_mut().push(task.scope); + // 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 @@ -26,5 +29,8 @@ impl VirtualDom { // Remove it from the scheduler tasks.try_remove(id.0); } + + // Remove the scope from the stack + self.runtime.scope_stack.borrow_mut().pop(); } } From 49001c59d39dd53170837020782221ddd406b5f4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 17:29:44 -0700 Subject: [PATCH 14/85] add logging to clock example --- examples/clock.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/clock.rs b/examples/clock.rs index 5c4bc4f25..2ed4812cf 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -8,6 +8,8 @@ fn main() { } fn app(cx: Scope) -> Element { + println!("running app"); + let counts = use_signal(cx, || (0..100).map(Signal::new).collect::>()); cx.use_hook(|| { @@ -17,8 +19,9 @@ fn app(cx: Scope) -> Element { }); render! { - for count in counts { + for (i, count) in counts.into_iter().enumerate() { Child { + id: i, count: count, } } @@ -27,10 +30,12 @@ fn app(cx: Scope) -> Element { #[derive(Props, PartialEq)] struct ChildProps { + id: usize, count: Signal, } fn Child(cx: Scope) -> Element { + println!("running child {}", cx.props.id); let count = cx.props.count; use_future!(cx, || async move { From dce418140a934bd68a1041b94752c96d4c256002 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Aug 2023 17:50:05 -0700 Subject: [PATCH 15/85] create a way to hoist ownership to a different scope --- packages/core/src/lib.rs | 10 +++++----- packages/core/src/scope_context.rs | 14 ++++++++++++++ packages/signals/src/lib.rs | 4 ++++ packages/signals/src/rt.rs | 31 +++++++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b65eaa86a..d500ca6b5 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -84,11 +84,11 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - consume_context, current_scope_id, fc_to_builder, has_context, provide_context, - provide_root_context, schedule_update_any, suspend, throw, 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, schedule_update_any, + 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/scope_context.rs b/packages/core/src/scope_context.rs index 13263f529..a724bb27a 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -268,6 +268,15 @@ 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() @@ -278,6 +287,11 @@ 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)) diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 4340b6484..a49a38a18 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -70,6 +70,10 @@ impl Signal { } } + pub fn origin_scope(&self) -> ScopeId { + self.inner.origin_scope() + } + pub fn read(&self) -> Ref { let inner = self.inner.read(); if let Some(current_scope_id) = current_scope_id() { diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index 0605fb951..ca5d5ec9f 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -2,7 +2,11 @@ use std::cell::{Ref, RefMut}; use std::rc::Rc; -use dioxus_core::prelude::{consume_context, provide_root_context}; +use dioxus_core::prelude::{ + consume_context, consume_context_from_scope, current_scope_id, provide_context_to_scope, + provide_root_context, +}; +use dioxus_core::ScopeId; use dioxus_copy::{CopyHandle, Owner, Store}; @@ -26,8 +30,19 @@ fn current_owner() -> Rc { } } +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") + } + } +} + pub struct CopyValue { pub value: CopyHandle, + origin_scope: ScopeId, } impl CopyValue { @@ -36,9 +51,23 @@ impl CopyValue { Self { value: owner.insert(value), + origin_scope: current_scope_id().expect("in a virtual dom"), } } + pub fn new_in_scope(value: T, scope: ScopeId) -> Self { + let owner = owner_in_scope(scope); + + Self { + value: owner.insert(value), + origin_scope: scope, + } + } + + pub fn origin_scope(&self) -> ScopeId { + self.origin_scope + } + pub fn try_read(&self) -> Option> { self.value.try_read() } From b0d499036a4d6fe21661d2906eba5c849bc29dd5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 11:16:22 -0700 Subject: [PATCH 16/85] make future handling functions global --- packages/core/src/lib.rs | 9 +++++---- packages/core/src/scope_context.rs | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d500ca6b5..363be390d 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -85,10 +85,11 @@ pub use crate::innerlude::{ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, - provide_context, provide_context_to_scope, provide_root_context, schedule_update_any, - suspend, throw, AnyValue, Component, Element, Event, EventHandler, Fragment, - IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, - Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + 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/scope_context.rs b/packages/core/src/scope_context.rs index a724bb27a..36c864117 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -309,3 +309,27 @@ pub fn suspend() -> Option> { 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)); +} From 717c09c4a33b774f9b6899eecd50f070391a1d39 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 11:52:54 -0700 Subject: [PATCH 17/85] create tests for signals --- packages/copy/src/lib.rs | 15 +++++ packages/signals/Cargo.toml | 5 ++ packages/signals/src/impls.rs | 8 +-- packages/signals/src/lib.rs | 10 ++++ packages/signals/src/rt.rs | 2 +- packages/signals/tests/create.rs | 60 +++++++++++++++++++ packages/signals/tests/effect.rs | 47 +++++++++++++++ packages/signals/tests/subscribe.rs | 92 +++++++++++++++++++++++++++++ 8 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 packages/signals/tests/create.rs create mode 100644 packages/signals/tests/effect.rs create mode 100644 packages/signals/tests/subscribe.rs diff --git a/packages/copy/src/lib.rs b/packages/copy/src/lib.rs index 3b091d30a..149c14434 100644 --- a/packages/copy/src/lib.rs +++ b/packages/copy/src/lib.rs @@ -1,5 +1,6 @@ use std::{ cell::{Cell, Ref, RefCell, RefMut}, + fmt::Debug, marker::PhantomData, rc::Rc, }; @@ -151,6 +152,20 @@ pub struct CopyHandle { _marker: PhantomData, } +impl Debug for CopyHandle { + 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 CopyHandle { #[inline(always)] fn validate(&self) -> bool { diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 11d5542d7..19f2039cc 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -9,3 +9,8 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } dioxus-copy = { workspace = true } +log.workspace = true +simple_logger = "4.2.0" + +[dev-dependencies] +dioxus = { workspace = true } diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index d54e5251f..8a3c9db8c 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -38,25 +38,25 @@ macro_rules! impls { impl + Copy + 'static> std::ops::AddAssign for $ty { fn add_assign(&mut self, rhs: T) { - self.set(self.value() + rhs); + self.with_mut(|v| *v = *v + rhs) } } impl + Copy + 'static> std::ops::SubAssign for $ty { fn sub_assign(&mut self, rhs: T) { - self.set(self.value() - rhs); + self.with_mut(|v| *v = *v - rhs) } } impl + Copy + 'static> std::ops::MulAssign for $ty { fn mul_assign(&mut self, rhs: T) { - self.set(self.value() * rhs); + self.with_mut(|v| *v = *v * rhs) } } impl + Copy + 'static> std::ops::DivAssign for $ty { fn div_assign(&mut self, rhs: T) { - self.set(self.value() / rhs); + self.with_mut(|v| *v = *v / rhs) } } diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index a49a38a18..c0d081f7d 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -77,6 +77,11 @@ impl Signal { pub fn read(&self) -> Ref { let inner = self.inner.read(); if let Some(current_scope_id) = current_scope_id() { + 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); @@ -98,6 +103,11 @@ impl Signal { { 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); } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index ca5d5ec9f..b37a46a53 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -41,7 +41,7 @@ fn owner_in_scope(scope: ScopeId) -> Rc { } pub struct CopyValue { - pub value: CopyHandle, + pub(crate) value: CopyHandle, origin_scope: ScopeId, } 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/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); + } + } +} From 7f2049b6479835cb9354d7d455426a63f6fbbd80 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 12:32:46 -0700 Subject: [PATCH 18/85] add memo --- packages/copy/src/lib.rs | 17 ++++++++++++ packages/signals/src/effect.rs | 11 +++++++- packages/signals/src/lib.rs | 12 ++++++-- packages/signals/src/memo.rs | 42 ++++++++++++++++++++++++++++ packages/signals/src/rt.rs | 9 ++++++ packages/signals/tests/memo.rs | 50 ++++++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 packages/signals/src/memo.rs create mode 100644 packages/signals/tests/memo.rs diff --git a/packages/copy/src/lib.rs b/packages/copy/src/lib.rs index 149c14434..b9b8aec06 100644 --- a/packages/copy/src/lib.rs +++ b/packages/copy/src/lib.rs @@ -209,6 +209,12 @@ impl CopyHandle { self.try_write().unwrap() } + pub fn set(&self, value: T) { + self.validate().then(|| { + *self.raw.data.borrow_mut() = Some(Box::new(value)); + }); + } + pub fn ptr_eq(&self, other: &Self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] { @@ -316,6 +322,17 @@ impl Owner { 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) -> CopyHandle { + let location = self.store.claim(); + CopyHandle { + raw: location, + #[cfg(any(debug_assertions, feature = "check_generation"))] + generation: location.generation.get(), + _marker: PhantomData, + } + } } impl Drop for Owner { diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 9bc9b33f2..06b02ffd4 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,3 +1,6 @@ +use core::{self, fmt::Debug}; +use std::fmt::{self, Formatter}; + use dioxus_core::prelude::*; use crate::CopyValue; @@ -19,7 +22,13 @@ pub(crate) fn get_effect_stack() -> EffectStack { #[derive(Copy, Clone, PartialEq)] pub struct Effect { - callback: CopyValue>, + 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 { diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index c0d081f7d..8319b4b8e 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -8,8 +8,9 @@ mod rt; pub use rt::*; mod effect; pub use effect::*; -#[macro_use] mod impls; +mod memo; +pub use memo::*; use dioxus_core::{ prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, @@ -55,7 +56,7 @@ struct SignalData { } pub struct Signal { - inner: CopyValue>, + pub(crate) inner: CopyValue>, } impl Signal { @@ -115,6 +116,11 @@ impl Signal { let subscribers = { std::mem::take(&mut *self.inner.read().effect_subscribers.borrow_mut()) }; for effect in subscribers { + log::trace!( + "Write on {:?} triggered effect {:?}", + self.inner.value, + effect + ); effect.try_run(); } @@ -122,7 +128,7 @@ impl Signal { RefMut::map(inner, |v| &mut v.value) } - pub fn set(&mut self, value: T) { + pub fn set(&self, value: T) { *self.write() = value; } diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs new file mode 100644 index 000000000..362f9cbd3 --- /dev/null +++ b/packages/signals/src/memo.rs @@ -0,0 +1,42 @@ +use dioxus_core::prelude::*; + +use crate::{get_effect_stack, CopyValue, Effect, Signal, SignalData}; + +pub fn use_memo(cx: &ScopeState, f: impl FnMut() -> R + 'static) -> Signal { + *cx.use_hook(|| memo(f)) +} + +pub fn memo(mut f: impl FnMut() -> R + 'static) -> Signal { + let state = Signal:: { + inner: CopyValue::invalid(), + }; + let effect = Effect { + callback: CopyValue::invalid(), + }; + + { + get_effect_stack().effects.write().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.write().pop(); + } + + effect.callback.value.set(Box::new(move || { + let value = f(); + let changed = { + let state = state.read(); + value != *state + }; + if changed { + state.set(value) + } + })); + + state +} diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index b37a46a53..db394af5e 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -64,6 +64,15 @@ impl CopyValue { } } + pub(crate) fn invalid() -> Self { + let owner = current_owner(); + + Self { + value: owner.invalid(), + origin_scope: current_scope_id().expect("in a virtual dom"), + } + } + pub fn origin_scope(&self) -> ScopeId { self.origin_scope } diff --git a/packages/signals/tests/memo.rs b/packages/signals/tests/memo.rs new file mode 100644 index 000000000..b0f91ee73 --- /dev/null +++ b/packages/signals/tests/memo.rs @@ -0,0 +1,50 @@ +#![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 memos_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); + let memo = cx.use_hook(move || { + to_owned![counter]; + memo(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); +} From 190c65b15d0604d25cffef93e56812ed5f742ac1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 14:31:03 -0700 Subject: [PATCH 19/85] fix memo and add tests --- examples/signals.rs | 2 +- packages/signals/src/impls.rs | 34 ++++++++---- packages/signals/src/lib.rs | 88 +++++++++++++++++++++++++----- packages/signals/src/memo.rs | 4 +- packages/signals/tests/memo.rs | 97 +++++++++++++++++++++++++++++++++- 5 files changed, 198 insertions(+), 27 deletions(-) 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/signals/src/impls.rs b/packages/signals/src/impls.rs index 8a3c9db8c..0ce739640 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -1,5 +1,5 @@ use crate::rt::CopyValue; -use crate::Signal; +use crate::{Signal, Write}; use std::cell::{Ref, RefMut}; @@ -104,10 +104,6 @@ macro_rules! impls { pub fn get(&self, index: usize) -> Option> { Ref::filter_map(self.read(), |v| v.get(index)).ok() } - - pub fn get_mut(&self, index: usize) -> Option> { - RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() - } } impl $ty> { @@ -130,10 +126,6 @@ macro_rules! impls { Ref::filter_map(self.read(), |v| v.as_ref()).ok() } - pub fn as_mut(&self) -> Option> { - RefMut::filter_map(self.write(), |v| v.as_mut()).ok() - } - pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { self.get_or_insert_with(|| default) } @@ -183,6 +175,18 @@ impl IntoIterator for CopyValue> { } } +impl CopyValue> { + pub fn get_mut(&self, index: usize) -> Option> { + RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() + } +} + +impl CopyValue> { + pub fn as_mut(&self) -> Option> { + RefMut::filter_map(self.write(), |v| v.as_mut()).ok() + } +} + pub struct CopySignalIterator { index: usize, value: Signal>, @@ -210,3 +214,15 @@ impl IntoIterator for Signal> { } } } + +impl Signal> { + pub fn get_mut(&self, index: usize) -> Option>> { + Write::filter_map(self.write(), |v| v.get_mut(index)) + } +} + +impl Signal> { + 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 8319b4b8e..179e0bef8 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,5 +1,6 @@ use std::{ cell::{Ref, RefCell, RefMut}, + ops::{Deref, DerefMut}, rc::Rc, sync::Arc, }; @@ -52,7 +53,7 @@ struct SignalData { subscribers: Rc>>, effect_subscribers: Rc>>, update_any: Arc, - value: T, + pub(crate) value: T, } pub struct Signal { @@ -77,7 +78,12 @@ impl Signal { pub fn read(&self) -> Ref { let inner = self.inner.read(); - if let Some(current_scope_id) = current_scope_id() { + 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() { log::trace!( "{:?} subscribed to {:?}", self.inner.value, @@ -91,16 +97,19 @@ impl Signal { inner.subscribers.borrow_mut().push(unsubscriber.scope); } } - if let Some(effect) = Effect::current() { - let mut effect_subscribers = inner.effect_subscribers.borrow_mut(); - if !effect_subscribers.contains(&effect) { - effect_subscribers.push(effect); - } - } Ref::map(inner, |v| &v.value) } - pub fn write(&self) -> RefMut { + 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() { @@ -113,8 +122,11 @@ impl Signal { } } - let subscribers = - { std::mem::take(&mut *self.inner.read().effect_subscribers.borrow_mut()) }; + 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 {:?}", @@ -123,9 +135,6 @@ impl Signal { ); effect.try_run(); } - - let inner = self.inner.write(); - RefMut::map(inner, |v| &mut v.value) } pub fn set(&self, value: T) { @@ -154,3 +163,54 @@ impl PartialEq for Signal { self.inner == other.inner } } + +struct SignalSubscriberDrop { + signal: Signal, +} + +impl Drop for SignalSubscriberDrop { + fn drop(&mut self) { + self.signal.update_subscribers(); + } +} + +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> { + 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, + } + } + + 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: 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 + } +} diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 362f9cbd3..07d18f5a6 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -30,8 +30,8 @@ pub fn memo(mut f: impl FnMut() -> R + 'static) -> Signal { effect.callback.value.set(Box::new(move || { let value = f(); let changed = { - let state = state.read(); - value != *state + let old = state.inner.read(); + value != old.value }; if changed { state.set(value) diff --git a/packages/signals/tests/memo.rs b/packages/signals/tests/memo.rs index b0f91ee73..b27d7d546 100644 --- a/packages/signals/tests/memo.rs +++ b/packages/signals/tests/memo.rs @@ -2,13 +2,14 @@ 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() { - simple_logger::SimpleLogger::new().init().unwrap(); + let _ = simple_logger::SimpleLogger::new().init(); #[derive(Default)] struct RunCounter { @@ -48,3 +49,97 @@ fn memos_rerun() { 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]; + memo(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); + } +} From 84734c4146fa71003e1949c22bd15cdce61337b1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 14:34:16 -0700 Subject: [PATCH 20/85] change dioxus signal to generational box --- Cargo.toml | 4 ++-- .../{copy => generational_box}/Cargo.toml | 2 +- .../{copy => generational_box}/src/lib.rs | 24 +++++++++---------- packages/signals/Cargo.toml | 2 +- packages/signals/src/rt.rs | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) rename packages/{copy => generational_box}/Cargo.toml (92%) rename packages/{copy => generational_box}/src/lib.rs (93%) diff --git a/Cargo.toml b/Cargo.toml index a544335f8..b01396a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", - "packages/copy", + "packages/generational_box", "packages/signals", "packages/hot-reload", "packages/fullstack", @@ -77,7 +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" } -dioxus-copy = { path = "packages/copy" } +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/packages/copy/Cargo.toml b/packages/generational_box/Cargo.toml similarity index 92% rename from packages/copy/Cargo.toml rename to packages/generational_box/Cargo.toml index 56964badd..6c5a5a841 100644 --- a/packages/copy/Cargo.toml +++ b/packages/generational_box/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dioxus-copy" +name = "generational-box" authors = ["Evan Almloff"] version = "0.0.0" edition = "2018" diff --git a/packages/copy/src/lib.rs b/packages/generational_box/src/lib.rs similarity index 93% rename from packages/copy/src/lib.rs rename to packages/generational_box/src/lib.rs index b9b8aec06..d45701e6c 100644 --- a/packages/copy/src/lib.rs +++ b/packages/generational_box/src/lib.rs @@ -106,8 +106,8 @@ fn panics() { fn fuzz() { fn maybe_owner_scope( store: &Store, - valid_keys: &mut Vec>, - invalid_keys: &mut Vec>, + valid_keys: &mut Vec>, + invalid_keys: &mut Vec>, path: &mut Vec, ) { let branch_cutoff = 5; @@ -145,14 +145,14 @@ fn fuzz() { } } -pub struct CopyHandle { +pub struct GenerationalBox { raw: MemoryLocation, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: u32, _marker: PhantomData, } -impl Debug for CopyHandle { +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!( @@ -166,7 +166,7 @@ impl Debug for CopyHandle { } } -impl CopyHandle { +impl GenerationalBox { #[inline(always)] fn validate(&self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] @@ -227,9 +227,9 @@ impl CopyHandle { } } -impl Copy for CopyHandle {} +impl Copy for GenerationalBox {} -impl Clone for CopyHandle { +impl Clone for GenerationalBox { fn clone(&self) -> Self { *self } @@ -253,13 +253,13 @@ impl MemoryLocation { } } - fn replace(&mut self, value: T) -> CopyHandle { + 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()); - CopyHandle { + GenerationalBox { raw: *self, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: self.generation.get(), @@ -316,7 +316,7 @@ pub struct Owner { } impl Owner { - pub fn insert(&self, value: T) -> CopyHandle { + pub fn insert(&self, value: T) -> GenerationalBox { let mut location = self.store.claim(); let key = location.replace(value); self.owned.borrow_mut().push(location); @@ -324,9 +324,9 @@ impl Owner { } /// 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) -> CopyHandle { + pub fn invalid(&self) -> GenerationalBox { let location = self.store.claim(); - CopyHandle { + GenerationalBox { raw: location, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: location.generation.get(), diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 19f2039cc..c145b850a 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] dioxus-core = { workspace = true } -dioxus-copy = { workspace = true } +generational-box = { workspace = true } log.workspace = true simple_logger = "4.2.0" diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index db394af5e..f96ff6ab3 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -8,7 +8,7 @@ use dioxus_core::prelude::{ }; use dioxus_core::ScopeId; -use dioxus_copy::{CopyHandle, Owner, Store}; +use generational_box::{GenerationalBox, Owner, Store}; fn current_store() -> Store { match consume_context() { @@ -41,7 +41,7 @@ fn owner_in_scope(scope: ScopeId) -> Rc { } pub struct CopyValue { - pub(crate) value: CopyHandle, + pub(crate) value: GenerationalBox, origin_scope: ScopeId, } From ddbe913e799ff80ef6be0444c408ee6a1af6682f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 14:50:36 -0700 Subject: [PATCH 21/85] make signals non-breaking --- packages/core/src/arena.rs | 8 +++++++- packages/core/src/events.rs | 11 ++++++++--- packages/core/src/nodes.rs | 6 ++++-- packages/core/src/scopes.rs | 29 +++++++++++++++++++---------- packages/core/src/virtual_dom.rs | 12 ++++++------ 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index b7ec46623..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) } @@ -163,7 +169,7 @@ impl VirtualDom { let listener = unsafe { &*listener }; match &listener.value { AttributeValue::Listener(l) => { - _ = l.callback.take(); + _ = l.take(); } AttributeValue::Any(a) => { _ = a.take(); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index c734ebd4a..90511a8e6 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,9 +1,8 @@ +use crate::{runtime::with_runtime, ScopeId}; use std::{ cell::{Cell, RefCell}, rc::Rc, - }; -use crate::ScopeId; /// A wrapper around some generic data that handles the event's state /// @@ -136,7 +135,7 @@ impl std::fmt::Debug for Event { /// } /// /// ``` -pub struct EventHandler<'bump, T:?Sized = ()> { +pub struct EventHandler<'bump, T: ?Sized = ()> { pub(crate) origin: ScopeId, pub(super) callback: RefCell>>, } @@ -158,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/nodes.rs b/packages/core/src/nodes.rs index 5406e8f07..a108f3577 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,4 @@ -use crate::prelude::EventHandler;use crate::{ +use crate::{ any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; use bumpalo::boxed::Box as BumpBox; @@ -476,7 +476,7 @@ pub enum AttributeValue<'a> { Bool(bool), /// A listener, like "onclick" - Listener(EventHandler<'a, Event>), + Listener(RefCell>>), /// An arbitrary value that implements PartialEq and is static Any(RefCell>>), @@ -485,6 +485,8 @@ pub enum AttributeValue<'a> { None, } +pub type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; + /// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements that are borrowed /// /// These varients are used to communicate what the value of an attribute is that needs to be updated diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index b43b2143a..f255c4175 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -446,7 +446,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, origin: self.context().id } + EventHandler { + callback, + origin: self.context().id, + } } /// Create a new [`AttributeValue`] with the listener variant from a callback @@ -456,16 +459,22 @@ impl<'src> ScopeState { &'src self, mut callback: impl FnMut(Event) + 'src, ) -> AttributeValue<'src> { - + // safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw + // This is the suggested way to build a bumpbox + // + // In theory, we could just use regular boxes + let boxed: BumpBox<'src, dyn FnMut(_) + 'src> = unsafe { + BumpBox::from_raw(self.bump().alloc(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propagates: event.propagates, + data, + }); + } + })) + }; - AttributeValue::Listener(self.event_handler(move |event: Event| { - if let Ok(data) = event.data.downcast::() { - callback(Event { - propagates: event.propagates, - data, - }); - } - })) + AttributeValue::Listener(RefCell::new(Some(boxed))) } /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c43af2254..f20b80412 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -391,12 +391,12 @@ 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 = listener.origin; - self.runtime.scope_stack.borrow_mut().push(origin); - if let Some(cb) = listener.callback.borrow_mut().as_deref_mut() { + let origin = el_ref.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { cb(uievent.clone()); } - self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.scope_stack.borrow_mut().pop(); if !uievent.propagates.get() { return; @@ -425,9 +425,9 @@ 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 = listener.origin; + let origin = el_ref.scope; self.runtime.scope_stack.borrow_mut().push(origin); - if let Some(cb) = listener.callback.borrow_mut().as_deref_mut() { + if let Some(cb) = listener.borrow_mut().as_deref_mut() { cb(uievent.clone()); } self.runtime.scope_stack.borrow_mut().pop(); From 6ca170453b6e20350787ffd6e2d4fbdea14b0433 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 14:59:58 -0700 Subject: [PATCH 22/85] handle virtual dom switching --- packages/core/src/runtime.rs | 15 +++++++++++++++ packages/core/src/scheduler/wait.rs | 3 ++- packages/core/src/virtual_dom.rs | 19 ++++++++++--------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 1ca778edd..89de770c5 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -89,3 +89,18 @@ impl Runtime { .ok() } } + +pub struct RuntimeGuard(Rc); + +impl RuntimeGuard { + pub 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 47180c240..62fa10180 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,6 +7,7 @@ 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 _runtime = RuntimeGuard::new(self.runtime.clone()); let mut tasks = self.runtime.scheduler.tasks.borrow_mut(); let task = match tasks.get(id.0) { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f20b80412..0a4577f4c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,7 +9,7 @@ use crate::{ mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, - runtime::{pop_runtime, push_runtime, Runtime}, + runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, AttributeValue, Element, Event, Scope, }; @@ -275,9 +275,6 @@ impl VirtualDom { // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(ElementRef::none()); - // Set this as the current runtime - push_runtime(dom.runtime.clone()); - dom } @@ -329,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 @@ -543,6 +542,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) => { @@ -621,9 +621,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 @@ -657,7 +660,5 @@ impl Drop for VirtualDom { fn drop(&mut self) { // Simply drop this scope which drops all of its children self.drop_scope(ScopeId(0), true); - // remove the current runtime - pop_runtime(); } } From 646c161c7d98ea62f46da5135fa838734d0646bf Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 16:04:49 -0700 Subject: [PATCH 23/85] create read only signal --- Cargo.toml | 4 +- .../Cargo.toml | 0 .../src/lib.rs | 0 packages/signals/src/impls.rs | 51 ++-- packages/signals/src/lib.rs | 211 +-------------- packages/signals/src/memo.rs | 13 +- packages/signals/src/signal.rs | 244 ++++++++++++++++++ 7 files changed, 287 insertions(+), 236 deletions(-) rename packages/{generational_box => generational-box}/Cargo.toml (100%) rename packages/{generational_box => generational-box}/src/lib.rs (100%) create mode 100644 packages/signals/src/signal.rs diff --git a/Cargo.toml b/Cargo.toml index b01396a26..7bb7d1570 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", - "packages/generational_box", + "packages/generational-box", "packages/signals", "packages/hot-reload", "packages/fullstack", @@ -77,7 +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" } +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/packages/generational_box/Cargo.toml b/packages/generational-box/Cargo.toml similarity index 100% rename from packages/generational_box/Cargo.toml rename to packages/generational-box/Cargo.toml diff --git a/packages/generational_box/src/lib.rs b/packages/generational-box/src/lib.rs similarity index 100% rename from packages/generational_box/src/lib.rs rename to packages/generational-box/src/lib.rs diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 0ce739640..0328d4063 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -1,5 +1,5 @@ use crate::rt::CopyValue; -use crate::{Signal, Write}; +use crate::signal::{ReadOnlySignal, Signal, Write}; use std::cell::{Ref, RefMut}; @@ -8,11 +8,11 @@ use std::{ ops::{Add, Div, Mul, Sub}, }; -macro_rules! impls { +macro_rules! read_impls { ($ty:ident) => { impl Default for $ty { fn default() -> Self { - Self::new(T::default()) + Self::new(Default::default()) } } @@ -36,6 +36,29 @@ macro_rules! impls { } } + impl $ty> { + pub fn get(&self, index: usize) -> Option> { + Ref::filter_map(self.read(), |v| v.get(index)).ok() + } + } + + impl $ty> { + pub fn unwrap(&self) -> T + where + T: Clone, + { + self.with(|v| v.clone()).unwrap() + } + + 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::AddAssign for $ty { fn add_assign(&mut self, rhs: T) { self.with_mut(|v| *v = *v + rhs) @@ -100,10 +123,6 @@ macro_rules! impls { pub fn split_off(&self, at: usize) -> Vec { self.with_mut(|v| v.split_off(at)) } - - pub fn get(&self, index: usize) -> Option> { - Ref::filter_map(self.read(), |v| v.get(index)).ok() - } } impl $ty> { @@ -115,17 +134,6 @@ macro_rules! impls { self.with_mut(|v| v.replace(value)) } - pub fn unwrap(&self) -> T - where - T: Clone, - { - self.with(|v| v.clone()).unwrap() - } - - pub fn as_ref(&self) -> Option> { - Ref::filter_map(self.read(), |v| v.as_ref()).ok() - } - pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { self.get_or_insert_with(|| default) } @@ -144,8 +152,11 @@ macro_rules! impls { }; } -impls!(CopyValue); -impls!(Signal); +read_impls!(CopyValue); +write_impls!(CopyValue); +read_impls!(Signal); +write_impls!(Signal); +read_impls!(ReadOnlySignal); pub struct CopyValueIterator { index: usize, diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 179e0bef8..66b7801f0 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,10 +1,3 @@ -use std::{ - cell::{Ref, RefCell, RefMut}, - ops::{Deref, DerefMut}, - rc::Rc, - sync::Arc, -}; - mod rt; pub use rt::*; mod effect; @@ -12,205 +5,5 @@ pub use effect::*; mod impls; mod memo; pub use memo::*; - -use dioxus_core::{ - prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, - ScopeId, ScopeState, -}; - -pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { - *cx.use_hook(|| Signal::new(f())) -} - -#[derive(Clone)] -struct Unsubscriber { - scope: ScopeId, - subscribers: 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") - } - } -} - -struct SignalData { - subscribers: Rc>>, - effect_subscribers: Rc>>, - update_any: Arc, - pub(crate) value: T, -} - -pub struct Signal { - pub(crate) inner: CopyValue>, -} - -impl Signal { - 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, - }), - } - } - - pub fn origin_scope(&self) -> ScopeId { - self.inner.origin_scope() - } - - 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() { - 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) - } - - 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(); - } - } - - pub fn set(&self, value: T) { - *self.write() = value; - } - - pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { - let write = self.read(); - f(&*write) - } - - pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { - let mut write = self.write(); - f(&mut *write) - } -} - -impl Signal { - 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(); - } -} - -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> { - 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, - } - } - - 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: 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 - } -} +pub(crate) mod signal; +pub use signal::*; diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 07d18f5a6..62292731a 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,12 +1,15 @@ use dioxus_core::prelude::*; -use crate::{get_effect_stack, CopyValue, Effect, Signal, SignalData}; +use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySignal, Signal}; -pub fn use_memo(cx: &ScopeState, f: impl FnMut() -> R + 'static) -> Signal { - *cx.use_hook(|| memo(f)) +pub fn use_selector( + cx: &ScopeState, + f: impl FnMut() -> R + 'static, +) -> ReadOnlySignal { + *cx.use_hook(|| selector(f)) } -pub fn memo(mut f: impl FnMut() -> R + 'static) -> Signal { +fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal { let state = Signal:: { inner: CopyValue::invalid(), }; @@ -38,5 +41,5 @@ pub fn memo(mut f: impl FnMut() -> R + 'static) -> Signal { } })); - state + ReadOnlySignal::new(state) } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs new file mode 100644 index 000000000..0c45da75a --- /dev/null +++ b/packages/signals/src/signal.rs @@ -0,0 +1,244 @@ +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}; + +pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { + *cx.use_hook(|| Signal::new(f())) +} + +#[derive(Clone)] +struct Unsubscriber { + scope: ScopeId, + subscribers: 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, +} + +pub struct Signal { + pub(crate) inner: CopyValue>, +} + +impl Signal { + 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, + }), + } + } + + pub fn origin_scope(&self) -> ScopeId { + self.inner.origin_scope() + } + + 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() { + 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) + } + + 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(); + } + } + + pub fn set(&self, value: T) { + *self.write() = value; + } + + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + f(&mut *write) + } +} + +impl Signal { + 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(); + } +} + +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> { + 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, + } + } + + 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: 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 + } +} + +pub struct ReadOnlySignal { + inner: Signal, +} + +impl ReadOnlySignal { + pub fn new(signal: Signal) -> Self { + Self { inner: signal } + } + + pub fn origin_scope(&self) -> ScopeId { + self.inner.origin_scope() + } + + pub fn read(&self) -> Ref { + self.inner.read() + } + + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + self.inner.with(f) + } +} + +impl ReadOnlySignal { + pub fn value(&self) -> T { + self.read().clone() + } +} + +impl PartialEq for ReadOnlySignal { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} From 2c7d0700d4dc2b3d926b98a94d6599a8a6f6c8d3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 16:17:47 -0700 Subject: [PATCH 24/85] rename memo to selector --- packages/signals/src/lib.rs | 4 ++-- packages/signals/src/{memo.rs => selector.rs} | 2 +- packages/signals/tests/{memo.rs => selector.rs} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/signals/src/{memo.rs => selector.rs} (92%) rename packages/signals/tests/{memo.rs => selector.rs} (98%) diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 66b7801f0..b667fe074 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -3,7 +3,7 @@ pub use rt::*; mod effect; pub use effect::*; mod impls; -mod memo; -pub use memo::*; +mod selector; +pub use selector::*; pub(crate) mod signal; pub use signal::*; diff --git a/packages/signals/src/memo.rs b/packages/signals/src/selector.rs similarity index 92% rename from packages/signals/src/memo.rs rename to packages/signals/src/selector.rs index 62292731a..dd02031ad 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/selector.rs @@ -9,7 +9,7 @@ pub fn use_selector( *cx.use_hook(|| selector(f)) } -fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal { +pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal { let state = Signal:: { inner: CopyValue::invalid(), }; diff --git a/packages/signals/tests/memo.rs b/packages/signals/tests/selector.rs similarity index 98% rename from packages/signals/tests/memo.rs rename to packages/signals/tests/selector.rs index b27d7d546..17eb1c38a 100644 --- a/packages/signals/tests/memo.rs +++ b/packages/signals/tests/selector.rs @@ -26,7 +26,7 @@ fn memos_rerun() { let mut signal = use_signal(cx, || 0); let memo = cx.use_hook(move || { to_owned![counter]; - memo(move || { + selector(move || { counter.borrow_mut().effect += 1; println!("Signal: {:?}", signal); signal.value() @@ -102,7 +102,7 @@ fn memos_prevents_component_rerun() { let memo = cx.use_hook(move || { to_owned![counter]; - memo(move || { + selector(move || { counter.borrow_mut().effect += 1; println!("Signal: {:?}", signal); signal.value() From cfd68bf7d9ac94851928f9772536a8897c6e90d0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 16:56:49 -0700 Subject: [PATCH 25/85] document signals crate --- packages/hooks/src/computed.rs | 210 ------------------------------- packages/hooks/src/lib.rs | 3 - packages/signals/src/effect.rs | 10 ++ packages/signals/src/impls.rs | 60 ++++++++- packages/signals/src/rt.rs | 6 + packages/signals/src/selector.rs | 20 +++ packages/signals/src/signal.rs | 31 +++++ 7 files changed, 123 insertions(+), 217 deletions(-) delete mode 100644 packages/hooks/src/computed.rs diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs deleted file mode 100644 index 248eb4f83..000000000 --- a/packages/hooks/src/computed.rs +++ /dev/null @@ -1,210 +0,0 @@ -use dioxus_core::{ScopeId, ScopeState}; -use slab::Slab; -use std::{ - cell::{RefCell, RefMut}, - collections::HashSet, - ops::{Deref, DerefMut}, - rc::Rc, -}; - -/// Create a new tracked state. -/// Tracked state is state that can drive Selector state -/// -/// It state will efficiently update any Selector state that is reading from it, but it is not readable on it's own. -/// -/// ```rust -/// use dioxus::prelude::*; -/// -/// fn Parent(cx: Scope) -> Element { -/// let count = use_tracked_state(cx, || 0); -/// -/// render! { -/// Child { -/// count: count.clone(), -/// } -/// } -/// } -/// -/// #[inline_props] -/// fn Child(cx: Scope, count: Tracked) -> Element { -/// let less_than_five = use_selector(cx, count, |count| *count < 5); -/// -/// render! { -/// "{less_than_five}" -/// } -/// } -/// ``` -pub fn use_tracked_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked { - cx.use_hook(|| { - let init = init(); - Tracked::new(cx, init) - }) -} - -/// Tracked state is state that can drive Selector state -/// -/// Tracked state will efficiently update any Selector state that is reading from it, but it is not readable on it's own. -#[derive(Clone)] -pub struct Tracked { - state: Rc>, - update_any: std::sync::Arc, - subscribers: SubscribedCallbacks, -} - -impl PartialEq for Tracked { - fn eq(&self, other: &Self) -> bool { - self.state == other.state - } -} - -impl Tracked { - /// Create a new tracked state - pub fn new(cx: &ScopeState, state: I) -> Self { - let subscribers = std::rc::Rc::new(std::cell::RefCell::new(Slab::new())); - Self { - state: Rc::new(RefCell::new(state)), - subscribers, - update_any: cx.schedule_update_any(), - } - } - - /// Create a new Selector state from this tracked state - pub fn compute( - &self, - mut compute: impl FnMut(&I) -> O + 'static, - ) -> Selector { - let subscribers = Rc::new(RefCell::new(HashSet::new())); - let state = Rc::new(RefCell::new(compute(&self.state.borrow()))); - let update_any = self.update_any.clone(); - - Selector { - value: state.clone(), - subscribers: subscribers.clone(), - _tracker: Rc::new(self.track(move |input_state| { - let new = compute(input_state); - let different = { - let state = state.borrow(); - *state != new - }; - if different { - let mut state = state.borrow_mut(); - *state = new; - for id in subscribers.borrow().iter().copied() { - (update_any)(id); - } - } - })), - } - } - - pub(crate) fn track(&self, update: impl FnMut(&I) + 'static) -> Tracker { - let mut subscribers = self.subscribers.borrow_mut(); - let id = subscribers.insert(Box::new(update)); - Tracker { - subscribers: self.subscribers.clone(), - id, - } - } - - /// Write to the tracked state - pub fn write(&self) -> TrackedMut<'_, I> { - TrackedMut { - state: self.state.borrow_mut(), - subscribers: self.subscribers.clone(), - } - } -} - -/// A mutable reference to tracked state -pub struct TrackedMut<'a, I> { - state: RefMut<'a, I>, - subscribers: SubscribedCallbacks, -} - -impl<'a, I> Deref for TrackedMut<'a, I> { - type Target = I; - - fn deref(&self) -> &Self::Target { - &self.state - } -} - -impl<'a, I> DerefMut for TrackedMut<'a, I> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state - } -} - -impl<'a, I> Drop for TrackedMut<'a, I> { - fn drop(&mut self) { - let state = self.state.deref(); - for (_, sub) in &mut *self.subscribers.borrow_mut() { - sub(state); - } - } -} - -type SubscribedCallbacks = std::rc::Rc>>>; - -pub(crate) struct Tracker { - subscribers: SubscribedCallbacks, - id: usize, -} - -impl Drop for Tracker { - fn drop(&mut self) { - let _ = self.subscribers.borrow_mut().remove(self.id); - } -} - -pub fn use_selector( - cx: &ScopeState, - tracked: &Tracked, - init: impl FnMut(&I) -> O + 'static, -) -> O { - let selector = cx.use_hook(|| tracked.compute(init)); - selector.use_state(cx) -} - -/// Selector state is state that is derived from tracked state -/// -/// Whenever the tracked state changes, the Selector state will be updated and any components reading from it will be rerun -#[derive(Clone)] -pub struct Selector { - _tracker: Rc>, - value: Rc>, - subscribers: Rc>>, -} - -impl PartialEq for Selector { - fn eq(&self, other: &Self) -> bool { - std::rc::Rc::ptr_eq(&self.value, &other.value) - } -} - -impl Selector { - /// Read the Selector state and subscribe to updates - pub fn use_state(&self, cx: &ScopeState) -> T { - cx.use_hook(|| { - let id = cx.scope_id(); - self.subscribers.borrow_mut().insert(id); - - ComputedRead { - scope: cx.scope_id(), - subscribers: self.subscribers.clone(), - } - }); - self.value.borrow().clone() - } -} - -struct ComputedRead { - scope: ScopeId, - subscribers: std::rc::Rc>>, -} - -impl Drop for ComputedRead { - fn drop(&mut self) { - self.subscribers.borrow_mut().remove(&self.scope); - } -} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 7e5798167..65f390eda 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -52,9 +52,6 @@ macro_rules! to_owned { }; } -mod computed; -pub use computed::*; - mod use_on_unmount; pub use use_on_unmount::*; diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 06b02ffd4..be57f5cfd 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -20,6 +20,13 @@ pub(crate) fn get_effect_stack() -> EffectStack { } } +/// 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)); +} + +/// 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>, @@ -36,6 +43,9 @@ impl Effect { get_effect_stack().effects.read().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)), diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 0328d4063..4e81a5a27 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -43,6 +43,7 @@ macro_rules! read_impls { } impl $ty> { + /// Unwraps the inner value and clones it. pub fn unwrap(&self) -> T where T: Clone, @@ -50,6 +51,7 @@ macro_rules! read_impls { 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() } @@ -59,6 +61,14 @@ macro_rules! read_impls { 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) @@ -71,73 +81,111 @@ macro_rules! write_impls { } } + 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() { @@ -158,6 +206,7 @@ read_impls!(Signal); write_impls!(Signal); read_impls!(ReadOnlySignal); +/// An iterator over the values of a `CopyValue>`. pub struct CopyValueIterator { index: usize, value: CopyValue>, @@ -198,12 +247,13 @@ impl CopyValue> { } } -pub struct CopySignalIterator { +/// An iterator over items in a `Signal>`. +pub struct SignalIterator { index: usize, value: Signal>, } -impl Iterator for CopySignalIterator { +impl Iterator for SignalIterator { type Item = T; fn next(&mut self) -> Option { @@ -214,12 +264,12 @@ impl Iterator for CopySignalIterator { } impl IntoIterator for Signal> { - type IntoIter = CopySignalIterator; + type IntoIter = SignalIterator; type Item = T; fn into_iter(self) -> Self::IntoIter { - CopySignalIterator { + SignalIterator { index: 0, value: self, } @@ -227,12 +277,14 @@ impl IntoIterator for Signal> { } 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/rt.rs b/packages/signals/src/rt.rs index f96ff6ab3..dc1c4a66f 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -40,12 +40,18 @@ fn owner_in_scope(scope: ScopeId) -> Rc { } } +/// CopyValue is a wrapper around a value to make the value mutable and Copy. +/// +/// It is internally backed by [`generational_box::GenerationalBox`]. pub struct CopyValue { pub(crate) value: GenerationalBox, origin_scope: ScopeId, } 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(); diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index dd02031ad..79a8f679b 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -2,6 +2,23 @@ use dioxus_core::prelude::*; 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, @@ -9,6 +26,9 @@ pub fn use_selector( *cx.use_hook(|| selector(f)) } +/// 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(), diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 0c45da75a..698ac8e12 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -12,6 +12,37 @@ use dioxus_core::{ 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())) } From 280d094f67f0de7856d81cd836f744147fde0c66 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 17:32:25 -0700 Subject: [PATCH 26/85] document generational box --- packages/generational-box/README.md | 34 ++++++++++++++++++++++++++++ packages/generational-box/src/lib.rs | 3 +++ 2 files changed, 37 insertions(+) create mode 100644 packages/generational-box/README.md diff --git a/packages/generational-box/README.md b/packages/generational-box/README.md new file mode 100644 index 000000000..d4600dd53 --- /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. + +There are three main types that 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 index d45701e6c..dedd62d75 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -145,6 +145,7 @@ fn fuzz() { } } +/// 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"))] @@ -268,6 +269,7 @@ impl MemoryLocation { } } +/// 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, @@ -310,6 +312,7 @@ impl Store { } } +/// 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>>, From 4447709ac85fba020fb1f49738a6b7d89189efb4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 17:49:14 -0700 Subject: [PATCH 27/85] add readmes --- packages/generational-box/README.md | 2 +- packages/generational-box/src/lib.rs | 11 ++++++ packages/signals/README.md | 56 ++++++++++++++++++++++++++++ packages/signals/src/lib.rs | 3 ++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/signals/README.md diff --git a/packages/generational-box/README.md b/packages/generational-box/README.md index d4600dd53..5ab7926ba 100644 --- a/packages/generational-box/README.md +++ b/packages/generational-box/README.md @@ -2,7 +2,7 @@ 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. -There are three main types that manage state in Generational Box: +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. diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index dedd62d75..29c7ebdb2 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -1,3 +1,6 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + use std::{ cell::{Cell, Ref, RefCell, RefMut}, fmt::Debug, @@ -180,6 +183,7 @@ impl GenerationalBox { } } + /// Try to read the value. Returns None if the value is no longer valid. pub fn try_read(&self) -> Option> { self.validate() .then(|| { @@ -191,10 +195,12 @@ impl GenerationalBox { .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(|| { @@ -206,16 +212,19 @@ impl GenerationalBox { .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"))] { @@ -304,6 +313,7 @@ impl Store { } } + /// 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(), @@ -319,6 +329,7 @@ pub struct Owner { } 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); diff --git a/packages/signals/README.md b/packages/signals/README.md new file mode 100644 index 000000000..e65c613fe --- /dev/null +++ b/packages/signals/README.md @@ -0,0 +1,56 @@ +# Dioxus Signals + +Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions in Dioxus. + +## 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 +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 + 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 +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! { + onclick: move |_| { + *signal.write() += 1; + } + 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}" + } +} +``` diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index b667fe074..1c67ab462 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,3 +1,6 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + mod rt; pub use rt::*; mod effect; From 0c17bdb73753443c0c4f57e8b2eb3f6c750d386f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 17:55:29 -0700 Subject: [PATCH 28/85] don't subscribe in tasks or event handlers --- packages/core/src/lib.rs | 9 +++++---- packages/core/src/runtime.rs | 5 ++++- packages/core/src/scheduler/wait.rs | 2 ++ packages/core/src/scope_context.rs | 6 ++++++ packages/core/src/virtual_dom.rs | 4 ++++ packages/signals/src/signal.rs | 25 ++++++++++++++----------- 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 363be390d..62e142f60 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,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 diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 89de770c5..9f031f287 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -1,4 +1,4 @@ -use std::cell::{Ref, RefCell}; +use std::cell::{Cell, Ref, RefCell}; use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId}; use std::rc::Rc; @@ -47,6 +47,7 @@ pub struct Runtime { // We use this to track the current scope pub(crate) scope_stack: RefCell>, + pub(crate) rendering: Cell, } impl Runtime { @@ -57,6 +58,8 @@ impl Runtime { scope_contexts: Default::default(), scope_stack: Default::default(), + + rendering: Cell::new(true), }); runtime } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 62fa10180..625873293 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -20,6 +20,7 @@ impl VirtualDom { // 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() { @@ -33,5 +34,6 @@ impl VirtualDom { // 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_context.rs b/packages/core/src/scope_context.rs index 36c864117..82b3fd4e2 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -263,6 +263,12 @@ 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() diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 0a4577f4c..a1def71d2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -392,10 +392,12 @@ impl VirtualDom { 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; @@ -426,10 +428,12 @@ impl VirtualDom { 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; } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 698ac8e12..8d2275b41 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -109,17 +109,20 @@ impl Signal { effect_subscribers.push(effect); } } else if let Some(current_scope_id) = current_scope_id() { - 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); + // 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) From ed05abc19652bccec80ba144946b8712be01c949 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 18:00:17 -0700 Subject: [PATCH 29/85] document remaining methods in the signal crate --- packages/signals/src/impls.rs | 3 ++ packages/signals/src/rt.rs | 10 +++++++ packages/signals/src/signal.rs | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 4e81a5a27..6427c2e2b 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -37,6 +37,7 @@ macro_rules! read_impls { } 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() } @@ -236,12 +237,14 @@ impl IntoIterator for CopyValue> { } 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() } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index dc1c4a66f..685a77204 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -61,6 +61,7 @@ impl CopyValue { } } + /// 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); @@ -79,35 +80,43 @@ impl CopyValue { } } + /// 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) @@ -115,6 +124,7 @@ impl CopyValue { } impl CopyValue { + /// Get the value. If the value has been dropped, this will panic. pub fn value(&self) -> T { self.read().clone() } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 8d2275b41..2bac8e1ea 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -81,11 +81,43 @@ pub(crate) struct SignalData { 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>, } 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 { @@ -97,10 +129,13 @@ impl Signal { } } + /// 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() { @@ -128,6 +163,8 @@ impl Signal { 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); @@ -165,15 +202,20 @@ impl Signal { } } + /// 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) @@ -181,6 +223,8 @@ impl Signal { } 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() } @@ -202,12 +246,14 @@ impl Drop for SignalSubscriberDrop { } } +/// 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 { @@ -216,6 +262,7 @@ impl<'a, T: 'static, I: 'static> Write<'a, T, I> { } } + /// 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>, @@ -243,29 +290,35 @@ impl DerefMut for Write<'_, T> { } } +/// 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() } From 292b3367dd58c636bf02018f1f7c92202ecd9cb9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 18:20:03 -0700 Subject: [PATCH 30/85] create some more compelling examples --- packages/core/src/scope_context.rs | 12 +- packages/generational-box/src/lib.rs | 1 + packages/signals/Cargo.toml | 1 + packages/signals/README.md | 22 +++ packages/signals/examples/context.rs | 25 +++ .../signals/examples/split_subscriptions.rs | 150 ++++++++++++++++++ 6 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 packages/signals/examples/context.rs create mode 100644 packages/signals/examples/split_subscriptions.rs diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 82b3fd4e2..fec34f914 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -180,14 +180,10 @@ impl ScopeContext { /// 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| { - // Walk upwards until there is no more parent - and tada we have the root - let mut parent = runtime.get_context(self.scope_id()).unwrap(); - while let Some(next_parent) = parent.parent_id { - parent = runtime.get_context(next_parent).unwrap(); - } - debug_assert_eq!(parent.scope_id(), ScopeId(0)); - - parent.provide_context(context) + runtime + .get_context(ScopeId(0)) + .unwrap() + .provide_context(context) }) .expect("Runtime to exist") } diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 29c7ebdb2..60bd41419 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -258,6 +258,7 @@ impl MemoryLocation { 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); } diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index c145b850a..3df7d34ca 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -14,3 +14,4 @@ simple_logger = "4.2.0" [dev-dependencies] dioxus = { workspace = true } +dioxus-desktop = { workspace = true } diff --git a/packages/signals/README.md b/packages/signals/README.md index e65c613fe..68cb8d80a 100644 --- a/packages/signals/README.md +++ b/packages/signals/README.md @@ -54,3 +54,25 @@ fn Child(cx: Scope) -> Element { } } ``` + +Because subscriptions happen when you read from (not create) the data, you can provide signals through the normal context API: + +```rust +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/context.rs b/packages/signals/examples/context.rs new file mode 100644 index 000000000..ddd52e691 --- /dev/null +++ b/packages/signals/examples/context.rs @@ -0,0 +1,25 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::{use_signal, Effect, 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/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" + } + } + } +} From 51b62dd33a3b499760d3857c2b830b98b99139aa Mon Sep 17 00:00:00 2001 From: Yolo <92775570+HashCookie@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:57:57 +0800 Subject: [PATCH 31/85] fix: readme awesome link (#1320) --- notes/README/ZH_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index ca37f7be1..c6bf0f3da 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -25,7 +25,7 @@ - + Awesome Page From e2c08a79aeeb7819d2cbaa7cdaa2b4072b3d7880 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 11:12:08 -0700 Subject: [PATCH 32/85] fix signal readme example --- packages/signals/README.md | 20 ++++++++++++++++---- packages/signals/examples/context.rs | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/signals/README.md b/packages/signals/README.md index 68cb8d80a..baac37867 100644 --- a/packages/signals/README.md +++ b/packages/signals/README.md @@ -7,12 +7,15 @@ Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions in 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 -fn App(cx: Scope) -> Element { +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 - signal + print!("{signal}"); }); render! { @@ -26,13 +29,19 @@ fn App(cx: Scope) -> Element { 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! { - onclick: move |_| { - *signal.write() += 1; + button { + onclick: move |_| { + *signal.write() += 1; + }, + "Increase" } for id in 0..10 { Child { @@ -58,6 +67,9 @@ fn Child(cx: Scope) -> Element { 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)); diff --git a/packages/signals/examples/context.rs b/packages/signals/examples/context.rs index ddd52e691..5e7ea831f 100644 --- a/packages/signals/examples/context.rs +++ b/packages/signals/examples/context.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] use dioxus::prelude::*; -use dioxus_signals::{use_signal, Effect, Signal}; +use dioxus_signals::Signal; fn main() { dioxus_desktop::launch(app); From d6089bbd35cf99d92324c8777f8d43e212452607 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:06:33 -0700 Subject: [PATCH 33/85] add optional dependency variants of selectors and effects --- packages/signals/Cargo.toml | 1 + packages/signals/examples/dependancies.rs | 38 +++++++++++++ packages/signals/src/dependency.rs | 67 +++++++++++++++++++++++ packages/signals/src/effect.rs | 27 ++++++++- packages/signals/src/lib.rs | 2 + packages/signals/src/selector.rs | 40 ++++++++++++++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 packages/signals/examples/dependancies.rs create mode 100644 packages/signals/src/dependency.rs diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 3df7d34ca..2c24758b5 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -15,3 +15,4 @@ simple_logger = "4.2.0" [dev-dependencies] dioxus = { workspace = true } dioxus-desktop = { workspace = true } +tokio = { version = "1", features = ["full"] } 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/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 index be57f5cfd..fdceb5211 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,9 +1,12 @@ use core::{self, fmt::Debug}; use std::fmt::{self, Formatter}; +use std::marker::PhantomData; use dioxus_core::prelude::*; -use crate::CopyValue; +use crate::dependency::Dep; +use crate::{dependency::Dependency, CopyValue}; +use crate::{use_signal, Signal}; #[derive(Default, Clone, Copy)] pub(crate) struct EffectStack { @@ -26,6 +29,28 @@ 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 { diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 1c67ab462..1f8bc3297 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -10,3 +10,5 @@ mod selector; pub use selector::*; pub(crate) mod signal; pub use signal::*; +mod dependency; +pub use dependency::*; diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index 79a8f679b..81fb50398 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -1,5 +1,7 @@ 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. @@ -26,6 +28,44 @@ pub fn use_selector( *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. From ba7a663eb65c0dcee8c04ddd44f8bdb7096535ca Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:18:15 -0700 Subject: [PATCH 34/85] fix some ownership issues --- packages/core/src/scope_context.rs | 14 ++++++-------- packages/signals/src/effect.rs | 18 +++++++++--------- packages/signals/src/rt.rs | 6 +++--- packages/signals/src/selector.rs | 4 ++-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index fec34f914..0861d063f 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -5,7 +5,7 @@ use crate::{ }; use rustc_hash::FxHashSet; use std::{ - any::{Any, TypeId}, + any::Any, cell::{Cell, RefCell}, fmt::Debug, future::Future, @@ -25,7 +25,7 @@ pub struct ScopeContext { pub(crate) height: u32, pub(crate) suspended: Cell, - pub(crate) shared_contexts: RefCell)>>, + pub(crate) shared_contexts: RefCell>>, pub(crate) tasks: Rc, pub(crate) spawned_tasks: RefCell>, @@ -99,9 +99,7 @@ impl ScopeContext { self.shared_contexts .borrow() .iter() - .find(|(k, _)| *k == TypeId::of::()) - .map(|(_, v)| v)? - .downcast_ref::() + .find_map(|any| any.downcast_ref::()) .cloned() } @@ -121,7 +119,7 @@ impl ScopeContext { .shared_contexts .borrow() .iter() - .find_map(|(_, any)| any.downcast_ref::()) + .find_map(|any| any.downcast_ref::()) { return Some(shared.clone()); } @@ -159,14 +157,14 @@ impl ScopeContext { // 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::() { + if let Some(ctx) = ctx.downcast_mut::() { std::mem::swap(ctx, &mut value.clone()); return value; } } // Else, just push it - contexts.push((TypeId::of::(), Box::new(value.clone()))); + contexts.push(Box::new(value.clone())); value } diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index fdceb5211..0685938a0 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,16 +1,16 @@ use core::{self, fmt::Debug}; +use std::cell::RefCell; use std::fmt::{self, Formatter}; -use std::marker::PhantomData; - +use std::rc::Rc; +// use dioxus_core::prelude::*; -use crate::dependency::Dep; +use crate::use_signal; use crate::{dependency::Dependency, CopyValue}; -use crate::{use_signal, Signal}; -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone)] pub(crate) struct EffectStack { - pub(crate) effects: CopyValue>, + pub(crate) effects: Rc>>, } pub(crate) fn get_effect_stack() -> EffectStack { @@ -65,7 +65,7 @@ impl Debug for Effect { impl Effect { pub(crate) fn current() -> Option { - get_effect_stack().effects.read().last().copied() + get_effect_stack().effects.borrow().last().copied() } /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. @@ -85,11 +85,11 @@ impl Effect { pub fn try_run(&self) { if let Some(mut callback) = self.callback.try_write() { { - get_effect_stack().effects.write().push(*self); + get_effect_stack().effects.borrow_mut().push(*self); } callback(); { - get_effect_stack().effects.write().pop(); + get_effect_stack().effects.borrow_mut().pop(); } } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index 685a77204..0f0b3a8c2 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -3,8 +3,8 @@ use std::cell::{Ref, RefMut}; use std::rc::Rc; use dioxus_core::prelude::{ - consume_context, consume_context_from_scope, current_scope_id, provide_context_to_scope, - provide_root_context, + consume_context, consume_context_from_scope, current_scope_id, provide_context, + provide_context_to_scope, provide_root_context, }; use dioxus_core::ScopeId; @@ -25,7 +25,7 @@ fn current_owner() -> Rc { Some(rt) => rt, None => { let owner = Rc::new(current_store().owner()); - provide_root_context(owner).expect("in a virtual dom") + provide_context(owner).expect("in a virtual dom") } } } diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index 81fb50398..301f4495e 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -78,7 +78,7 @@ pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig }; { - get_effect_stack().effects.write().push(effect); + get_effect_stack().effects.borrow_mut().push(effect); } state.inner.value.set(SignalData { subscribers: Default::default(), @@ -87,7 +87,7 @@ pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig value: f(), }); { - get_effect_stack().effects.write().pop(); + // get_effect_stack().effects.borrow_mut().pop(); } effect.callback.value.set(Box::new(move || { From 69dab86873a77caa8bddaecfae1e8bfafdc5e34b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:27:45 -0700 Subject: [PATCH 35/85] add selector example to readme --- packages/core/src/scope_context.rs | 2 +- packages/signals/README.md | 34 ++++++++++++++++++++++++++- packages/signals/examples/selector.rs | 30 +++++++++++++++++++++++ packages/signals/src/selector.rs | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 packages/signals/examples/selector.rs diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 0861d063f..d627eea5f 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -245,7 +245,7 @@ impl ScopeContext { /// 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. +/// 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> { diff --git a/packages/signals/README.md b/packages/signals/README.md index baac37867..020edc18f 100644 --- a/packages/signals/README.md +++ b/packages/signals/README.md @@ -1,6 +1,6 @@ # Dioxus Signals -Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions in Dioxus. +Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions. ## Copy Data @@ -86,5 +86,37 @@ fn Child(cx: Scope) -> Element { "{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/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/src/selector.rs b/packages/signals/src/selector.rs index 301f4495e..5eb3fa3c9 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -87,7 +87,7 @@ pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig value: f(), }); { - // get_effect_stack().effects.borrow_mut().pop(); + get_effect_stack().effects.borrow_mut().pop(); } effect.callback.value.set(Box::new(move || { From 14c852010bc695c62884cffb0bc8ace70edf5fa2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:31:38 -0700 Subject: [PATCH 36/85] restore clock example --- examples/clock.rs | 59 +++++++---------------------------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index 2ed4812cf..f9c1892d9 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,65 +1,22 @@ -#![allow(non_snake_case)] - use dioxus::prelude::*; -use dioxus_signals::{use_signal, Effect, Signal}; +use dioxus_signals::use_signal; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - println!("running app"); - - let counts = use_signal(cx, || (0..100).map(Signal::new).collect::>()); - - cx.use_hook(|| { - Effect::new(move || { - println!("Counts: {:?}", counts); - }) - }); - - render! { - for (i, count) in counts.into_iter().enumerate() { - Child { - id: i, - count: count, - } - } - } -} - -#[derive(Props, PartialEq)] -struct ChildProps { - id: usize, - count: Signal, -} - -fn Child(cx: Scope) -> Element { - println!("running child {}", cx.props.id); - let count = cx.props.count; + let mut count = use_signal(cx, || 0); use_future!(cx, || async move { loop { - tokio::time::sleep(std::time::Duration::from_secs(count.value())).await; - *count.write() += 1; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + count += 1; + println!("current: {count}"); } }); - render! { - div { - "Child: {count}" - button { - onclick: move |_| { - *count.write() += 1; - }, - "Increase" - } - button { - onclick: move |_| { - *count.write() -= 1; - }, - "Decrease" - } - } - } + cx.render(rsx! { + div { "High-Five counter: {count}" } + }) } From 4ee4cf23d3fd64839b9e0080eddf4d6fe6a43a79 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:37:29 -0700 Subject: [PATCH 37/85] restore a few unneeded breaking changes --- packages/core/src/events.rs | 2 +- packages/core/src/runtime.rs | 6 +- packages/core/src/scope_context.rs | 2 +- packages/core/src/scopes.rs | 2 +- packages/hooks/src/computed.rs | 210 +++++++++++++++++++++++++++++ packages/hooks/src/lib.rs | 3 + 6 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 packages/hooks/src/computed.rs diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 90511a8e6..b85e0394d 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -135,7 +135,7 @@ impl std::fmt::Debug for Event { /// } /// /// ``` -pub struct EventHandler<'bump, T: ?Sized = ()> { +pub struct EventHandler<'bump, T = ()> { pub(crate) origin: ScopeId, pub(super) callback: RefCell>>, } diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 9f031f287..256b46909 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -41,7 +41,7 @@ where .flatten() } -pub struct Runtime { +pub(crate) struct Runtime { pub(crate) scope_contexts: RefCell>>, pub(crate) scheduler: Rc, @@ -93,10 +93,10 @@ impl Runtime { } } -pub struct RuntimeGuard(Rc); +pub(crate) struct RuntimeGuard(Rc); impl RuntimeGuard { - pub fn new(runtime: Rc) -> Self { + pub(crate) fn new(runtime: Rc) -> Self { push_runtime(runtime.clone()); Self(runtime) } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index d627eea5f..14366eefb 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -16,7 +16,7 @@ use std::{ /// A component's state separate from its props. /// /// This struct exists to provide a common interface for all scopes without relying on generics. -pub struct ScopeContext { +pub(crate) struct ScopeContext { pub(crate) name: &'static str, pub(crate) id: ScopeId, diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f255c4175..8469781c4 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -195,7 +195,7 @@ impl<'src> ScopeState { /// /// assert_eq!(base.parent(), None); /// ``` - pub fn parent_id(&self) -> Option { + pub fn parent(&self) -> Option { // safety: the pointer to our parent is *always* valid thanks to the bump arena self.context().parent_id() } diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs new file mode 100644 index 000000000..94011d37d --- /dev/null +++ b/packages/hooks/src/computed.rs @@ -0,0 +1,210 @@ +use dioxus_core::{ScopeId, ScopeState}; +use slab::Slab; +use std::{ + cell::{RefCell, RefMut}, + collections::HashSet, + ops::{Deref, DerefMut}, + rc::Rc, +}; + +/// Create a new tracked state. +/// Tracked state is state that can drive Selector state +/// +/// It will efficiently update any Selector state that is reading from it, but it is not readable on its own. +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn Parent(cx: Scope) -> Element { +/// let count = use_tracked_state(cx, || 0); +/// +/// render! { +/// Child { +/// count: count.clone(), +/// } +/// } +/// } +/// +/// #[inline_props] +/// fn Child(cx: Scope, count: Tracked) -> Element { +/// let less_than_five = use_selector(cx, count, |count| *count < 5); +/// +/// render! { +/// "{less_than_five}" +/// } +/// } +/// ``` +pub fn use_tracked_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked { + cx.use_hook(|| { + let init = init(); + Tracked::new(cx, init) + }) +} + +/// Tracked state is state that can drive Selector state +/// +/// Tracked state will efficiently update any Selector state that is reading from it, but it is not readable on it's own. +#[derive(Clone)] +pub struct Tracked { + state: Rc>, + update_any: std::sync::Arc, + subscribers: SubscribedCallbacks, +} + +impl PartialEq for Tracked { + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl Tracked { + /// Create a new tracked state + pub fn new(cx: &ScopeState, state: I) -> Self { + let subscribers = std::rc::Rc::new(std::cell::RefCell::new(Slab::new())); + Self { + state: Rc::new(RefCell::new(state)), + subscribers, + update_any: cx.schedule_update_any(), + } + } + + /// Create a new Selector state from this tracked state + pub fn compute( + &self, + mut compute: impl FnMut(&I) -> O + 'static, + ) -> Selector { + let subscribers = Rc::new(RefCell::new(HashSet::new())); + let state = Rc::new(RefCell::new(compute(&self.state.borrow()))); + let update_any = self.update_any.clone(); + + Selector { + value: state.clone(), + subscribers: subscribers.clone(), + _tracker: Rc::new(self.track(move |input_state| { + let new = compute(input_state); + let different = { + let state = state.borrow(); + *state != new + }; + if different { + let mut state = state.borrow_mut(); + *state = new; + for id in subscribers.borrow().iter().copied() { + (update_any)(id); + } + } + })), + } + } + + pub(crate) fn track(&self, update: impl FnMut(&I) + 'static) -> Tracker { + let mut subscribers = self.subscribers.borrow_mut(); + let id = subscribers.insert(Box::new(update)); + Tracker { + subscribers: self.subscribers.clone(), + id, + } + } + + /// Write to the tracked state + pub fn write(&self) -> TrackedMut<'_, I> { + TrackedMut { + state: self.state.borrow_mut(), + subscribers: self.subscribers.clone(), + } + } +} + +/// A mutable reference to tracked state +pub struct TrackedMut<'a, I> { + state: RefMut<'a, I>, + subscribers: SubscribedCallbacks, +} + +impl<'a, I> Deref for TrackedMut<'a, I> { + type Target = I; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl<'a, I> DerefMut for TrackedMut<'a, I> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + +impl<'a, I> Drop for TrackedMut<'a, I> { + fn drop(&mut self) { + let state = self.state.deref(); + for (_, sub) in &mut *self.subscribers.borrow_mut() { + sub(state); + } + } +} + +type SubscribedCallbacks = std::rc::Rc>>>; + +pub(crate) struct Tracker { + subscribers: SubscribedCallbacks, + id: usize, +} + +impl Drop for Tracker { + fn drop(&mut self) { + let _ = self.subscribers.borrow_mut().remove(self.id); + } +} + +pub fn use_selector( + cx: &ScopeState, + tracked: &Tracked, + init: impl FnMut(&I) -> O + 'static, +) -> O { + let selector = cx.use_hook(|| tracked.compute(init)); + selector.use_state(cx) +} + +/// Selector state is state that is derived from tracked state +/// +/// Whenever the tracked state changes, the Selector state will be updated and any components reading from it will be rerun +#[derive(Clone)] +pub struct Selector { + _tracker: Rc>, + value: Rc>, + subscribers: Rc>>, +} + +impl PartialEq for Selector { + fn eq(&self, other: &Self) -> bool { + std::rc::Rc::ptr_eq(&self.value, &other.value) + } +} + +impl Selector { + /// Read the Selector state and subscribe to updates + pub fn use_state(&self, cx: &ScopeState) -> T { + cx.use_hook(|| { + let id = cx.scope_id(); + self.subscribers.borrow_mut().insert(id); + + ComputedRead { + scope: cx.scope_id(), + subscribers: self.subscribers.clone(), + } + }); + self.value.borrow().clone() + } +} + +struct ComputedRead { + scope: ScopeId, + subscribers: std::rc::Rc>>, +} + +impl Drop for ComputedRead { + fn drop(&mut self) { + self.subscribers.borrow_mut().remove(&self.scope); + } +} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 65f390eda..7e5798167 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -52,6 +52,9 @@ macro_rules! to_owned { }; } +mod computed; +pub use computed::*; + mod use_on_unmount; pub use use_on_unmount::*; From 24c626b30660c42c64b6ec4aa9e5f1c7faf0f6ab Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 13:43:57 -0700 Subject: [PATCH 38/85] fix clippy --- packages/core/src/runtime.rs | 9 ++++----- packages/desktop/src/protocol.rs | 9 +++++++-- packages/dioxus-tui/examples/hover.rs | 8 +++++++- packages/hooks/src/computed.rs | 2 ++ packages/hooks/src/lib.rs | 3 +-- packages/html/src/eval.rs | 2 +- packages/native-core/src/real_dom.rs | 1 - packages/rsx/src/lib.rs | 10 ++-------- packages/signals/src/signal.rs | 13 ++++++------- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 256b46909..b2b535830 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -24,7 +24,7 @@ where { RUNTIMES.with(|stack| { let stack = stack.borrow(); - stack.last().map(|r| f(&**r)) + stack.last().map(|r| f(r)) }) } @@ -36,7 +36,7 @@ where with_runtime(|runtime| { runtime .current_scope_id() - .and_then(|scope| runtime.get_context(scope).map(|sc| f(&*sc))) + .and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc))) }) .flatten() } @@ -52,7 +52,7 @@ pub(crate) struct Runtime { impl Runtime { pub(crate) fn new(scheduler: Rc) -> Rc { - let runtime = Rc::new(Self { + Rc::new(Self { scheduler, scope_contexts: Default::default(), @@ -60,8 +60,7 @@ impl Runtime { scope_stack: Default::default(), rendering: Cell::new(true), - }); - runtime + }) } /// Create a scope context. This slab is synchronized with the scope slab. 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..c8784a8bc 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!(string, "{c:02X?}").unwrap(); + } + result } fn get_brightness(m: &Rc) -> i32 { 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/src/signal.rs b/packages/signals/src/signal.rs index 2bac8e1ea..5d696ee17 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -50,9 +50,11 @@ pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal< #[derive(Clone)] struct Unsubscriber { scope: ScopeId, - subscribers: Rc>>>>>, + subscribers: UnsubscriberArray, } +type UnsubscriberArray = Rc>>>>>; + impl Drop for Unsubscriber { fn drop(&mut self) { for subscribers in self.subscribers.borrow().iter() { @@ -269,10 +271,7 @@ impl<'a, T: 'static, I: 'static> Write<'a, T, I> { ) -> Option> { let Self { write, signal } = myself; let write = RefMut::filter_map(write, f).ok(); - write.map(|write| Write { - write, - signal: signal, - }) + write.map(|write| Write { write, signal }) } } @@ -280,13 +279,13 @@ impl<'a, T: 'static> Deref for Write<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { - &*self.write + &self.write } } impl DerefMut for Write<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut *self.write + &mut self.write } } From 3f31b9de43ee33187f530e49c87fab97a80e9876 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 15:13:02 -0700 Subject: [PATCH 39/85] fix tests --- packages/dioxus-tui/examples/hover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dioxus-tui/examples/hover.rs b/packages/dioxus-tui/examples/hover.rs index c8784a8bc..3af2d1616 100644 --- a/packages/dioxus-tui/examples/hover.rs +++ b/packages/dioxus-tui/examples/hover.rs @@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element { let mut result = String::new(); result += "#"; for c in c.iter() { - write!(string, "{c:02X?}").unwrap(); + write!(result, "{c:02X?}").unwrap(); } result } From f7017fac2ec57b30b0188ab9d515d9ceb37ef9bf Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 8 Aug 2023 15:45:09 -0700 Subject: [PATCH 40/85] Fix the login form example --- examples/login_form.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 74e634d5d..5ee47e4b5 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -35,7 +35,6 @@ fn app(cx: Scope) -> Element { h1 { "Login" } form { onsubmit: onsubmit, - prevent_default: "onsubmit", // Prevent the default behavior of

to post input { r#type: "text", id: "username", name: "username" } label { "Username" } br {} From 2a32271fa0f55b8f1238d5b3ac5b186d6725dd3b Mon Sep 17 00:00:00 2001 From: gmorenz Date: Tue, 8 Aug 2023 22:04:15 -0400 Subject: [PATCH 41/85] Set response headers from response, not request, parts (#1328) --- packages/fullstack/src/adapters/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fullstack/src/adapters/mod.rs b/packages/fullstack/src/adapters/mod.rs index 9133f8a08..98d190919 100644 --- a/packages/fullstack/src/adapters/mod.rs +++ b/packages/fullstack/src/adapters/mod.rs @@ -112,7 +112,7 @@ impl Service for ServerFnHandler { let mut res = http::Response::builder(); // Set the headers from the server context - let parts = server_context.parts.read().unwrap(); + let parts = server_context.response_parts().unwrap(); *res.headers_mut().expect("empty headers should be valid") = parts.headers.clone(); let serialized = result?; From c6ec2a9a9b37d72978636fee70e7231cdee7e0e5 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Thu, 10 Aug 2023 01:20:19 +0900 Subject: [PATCH 42/85] docs: add Japanese README (#1329) * Add Japanese README * Add links * minor fix --- README.md | 2 + notes/README/ZH_CN.md | 2 + translations/ja-jp/README.md | 178 +++++++++++++++++++++++++++++++++++ translations/pt-br/README.md | 2 + 4 files changed, 184 insertions(+) create mode 100644 translations/ja-jp/README.md diff --git a/README.md b/README.md index 28f27cd23..52e4408d8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ 中文 | PT-BR + | + 日本語 diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index c6bf0f3da..1c4abc667 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -45,6 +45,8 @@ English | PT-BR + | + 日本語 diff --git a/translations/ja-jp/README.md b/translations/ja-jp/README.md new file mode 100644 index 000000000..342a99cdb --- /dev/null +++ b/translations/ja-jp/README.md @@ -0,0 +1,178 @@ +

+ +

+ +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + + + + + Awesome Page + + + + Discord Link + +
+ + + +
+ +Dioxus は、Rust でクロスプラットフォームのユーザー・インターフェースを構築するための、移植性が高く、パフォーマンスと人間工学に基づいたフレームワークです。 + +```rust +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) +} +``` + +Dioxus は、ウェブアプリ、デスクトップアプリ、静的サイト、モバイルアプリ、TUI アプリ、ライブビューアプリなどの配信に使用できます。Dioxus は完全にレンダラーに依存せず、あらゆるレンダラーのプラットフォームとして使用可能です。 + +React を知っているのであれば、Dioxus はすでにご存知でしょう。 + +## ユニークな特徴: + +- 10 行以下のコードでネイティブに動作するデスクトップアプリ(Electron は不要!)。 +- 人間工学に基づいたパワフルな状態管理。 +- 包括的なインラインドキュメント - すべての HTML 要素、リスナー、イベントのホバーとガイド。 +- 驚異的な速さ 🔥 🔥 と極めて高いメモリ効率 +- 高速反復のための統合されたホットリロード +- コルーチンとサスペンスによるファーストクラスの非同期サポート +- などなど![リリース記事全文](https://dioxuslabs.com/blog/introducing-dioxus/)を読む。 + +## 対応プラットフォーム + +
+ + + + + + + + + + + + + + + + + + + + + +
Web +
    +
  • WebAssembly を使って DOM に直接レンダリングする
  • +
  • SSR でプレレンダリングし、クライアントの上で再ハイドレーション
  • +
  • シンプルな "hello world "は約 65kb で、React と同等
  • +
  • 開発サーバーを内蔵し、ホットリロードで迅速なイテレーションを実現
  • +
+
デスクトップ +
    +
  • ウェブビューを使ったレンダリング、あるいは実験的に WGPU や Skia を使ったレンダリング
  • +
  • 設定不要。cargo-run を実行するだけで、アプリをビルド可能
  • +
  • 電子的な IPC を必要としないネイティブシステムアクセスの完全サポート
  • +
  • macOS、Linux、Windows に対応。ポータブルで 3 mb より小さなバイナリ
  • +
+
モバイル +
    +
  • ウェブビューを使ったレンダリング、あるいは実験的に WGPU や Skia を使ったレンダリング
  • +
  • iOS と Android をサポート
  • +
  • React ネイティブよりも大幅にパフォーマンスが高い
  • +
+
ライブビュー +
    +
  • アプリケーション、または単一のコンポーネントを完全にサーバー上でレンダリング
  • +
  • Axum や Warp のような人気のある Rust フレームワークとの統合
  • +
  • 極めて低いレイテンシーと 10,000 以上の同時アプリをサポートする能力
  • +
+
ターミナル +
    +
  • ink.js のように、アプリケーションをターミナルに直接レンダリング
  • +
  • ブラウザでおなじみのフレックスボックスと CSS モデルを採用
  • +
  • テキスト入力、ボタン、フォーカスシステムなどの組み込みウィジェット
  • +
+
+
+ +## なぜ Dioxus? + +アプリを構築するための選択肢は山ほどあるのに、なぜ Dioxus を選ぶのでしょうか? + +まず第一に、Dioxus は開発者の体験を優先しています。これは Dioxus 独自のさまざまな機能に反映されています: + +- メタ言語(RSX)とそれに付随する VSCode 拡張機能のオートフォーマット機能 +- デスクトップとウェブの両方で RSX のインタプリタを使用したホットリロード +- 優れたドキュメントの重視 - 私たちのガイドは完全で、私たちの HTML 要素は文書化されています。 +- 簡素化のための多大な研究 + +Dioxus は非常に拡張性の高いプラットフォームでもあります。 + +- 非常にシンプルに最適化されたスタックマシンを実装することで、新しいレンダラーを簡単に構築できます。 +- コンポーネントやカスタム・エレメントもビルドして共有できます + +Dioxus は素晴らしいが、なぜ私には使えないのか? + +- まだ完全に成熟していません。API はまだ移行中で、いろいろなものが壊れるかもしれません(私たちはそれを避けようとしていますが)。 +- no-std 環境で動作させる必要がある。 +- UI を構築する React-hooks モデルが好きではない場合 + +## コントリビュート + +- 私たちの [issue tracker](https://github.com/dioxuslabs/dioxus/issues) で問題を報告してください。 +- ディスコードに参加して質問してください! + + + + + +## ライセンス + +このプロジェクトのライセンスは [MIT ライセンス] です。 + +[MIT ライセンス]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +あなたが明示的に別段の定めをしない限り、あなたによって Dioxus に含めるために意図的に提出されたコントリビュートは、 +いかなる追加条件もなく、MIT としてライセンスされるものとします。 diff --git a/translations/pt-br/README.md b/translations/pt-br/README.md index a50727662..2e4376bfb 100644 --- a/translations/pt-br/README.md +++ b/translations/pt-br/README.md @@ -43,6 +43,8 @@ Guia | 中文 + | + 日本語 From 92a8669e14c5441a0aa3ac6db9ae437dd03d1522 Mon Sep 17 00:00:00 2001 From: Dan Lock Date: Wed, 9 Aug 2023 17:26:50 +0100 Subject: [PATCH 43/85] Make launch functions pub (#1288) * Make launch functions pub * add docs to web, desktop, and server launch functions --------- Co-authored-by: Evan Almloff --- packages/fullstack/src/launch.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 8d125680b..c982a1c90 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -119,21 +119,22 @@ impl Date: Wed, 9 Aug 2023 10:26:59 -0700 Subject: [PATCH 44/85] fix variance on components --- packages/core/src/scopes.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 9470c3d0d..4a21fcb78 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -481,19 +481,20 @@ impl<'src> ScopeState { /// fn(Scope) -> Element; /// async fn(Scope>) -> Element; /// ``` - pub fn component

( + pub fn component<'child, P>( &'src self, - component: fn(Scope<'src, P>) -> Element<'src>, + component: fn(Scope<'child, P>) -> Element<'child>, props: P, fn_name: &'static str, ) -> DynamicNode<'src> where - P: Properties + 'src, + P: Properties + 'child, + 'src: 'child, { let vcomp = VProps::new(component, P::memoize, props); // cast off the lifetime of the render return - let as_dyn: Box + '_> = Box::new(vcomp); + let as_dyn: Box + '_> = Box::new(vcomp); let extended: Box + 'src> = unsafe { std::mem::transmute(as_dyn) }; DynamicNode::Component(VComponent { From 91500f0349044a1895f8da809f98d2366fd8f0aa Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 9 Aug 2023 10:29:19 -0700 Subject: [PATCH 45/85] add props safety example --- examples/props_safety.rs | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/props_safety.rs diff --git a/examples/props_safety.rs b/examples/props_safety.rs new file mode 100644 index 000000000..7802e75c9 --- /dev/null +++ b/examples/props_safety.rs @@ -0,0 +1,68 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let count: &RefCell> = cx.use_hook(|| RefCell::new(Vec::new())); + + let element = render! { + div { + "hello world!" + } + }; + + let nested_borrows = cx.use_hook(Vec::new); + nested_borrows.push("hello world!"); + + cx.render(rsx! { + // unsafe_child_component { + // borrowed: count + // } + safe_child_component { + borrowed: element + } + safe_nested_borrows { + borrowed: &**nested_borrows + } + }) +} + +#[derive(Props)] +struct NestedBorrows<'a> { + borrowed: &'a [&'a str], +} + +fn safe_nested_borrows<'a>(cx: Scope<'a, NestedBorrows<'a>>) -> Element<'a> { + render! { + div { "{cx.props.borrowed:?}" } + } +} + +#[derive(Props)] +struct Testing<'a> { + borrowed: &'a RefCell>>, +} + +fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> { + let Testing { borrowed } = cx.props; + let borrowed = borrowed.borrow(); + cx.render(rsx! { + div { "Hello, world!" } + }) +} + +#[derive(Props)] +struct SafeTesting<'a> { + borrowed: Element<'a>, +} + +fn safe_child_component<'a>(cx: Scope<'a, SafeTesting<'a>>) -> Element<'a> { + let SafeTesting { borrowed } = cx.props; + cx.render(rsx! { + div { + borrowed + } + }) +} From f91eeccca784ea2c64c751148d052f513b2c77d2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 9 Aug 2023 10:49:47 -0700 Subject: [PATCH 46/85] add compile test for unsound case --- packages/core/Cargo.toml | 1 + packages/core/compile_tests/props_safety.rs | 32 +++++++++++++++++++ .../core/compile_tests/props_safety.stderr | 20 ++++++++++++ packages/core/src/properties.rs | 6 ++++ 4 files changed, 59 insertions(+) create mode 100644 packages/core/compile_tests/props_safety.rs create mode 100644 packages/core/compile_tests/props_safety.stderr diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 7c9515fc0..71afe5825 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -39,6 +39,7 @@ dioxus = { workspace = true } pretty_assertions = "1.3.0" rand = "0.8.5" dioxus-ssr = { workspace = true } +trybuild = "1.0" [features] default = [] diff --git a/packages/core/compile_tests/props_safety.rs b/packages/core/compile_tests/props_safety.rs new file mode 100644 index 000000000..a9729cec9 --- /dev/null +++ b/packages/core/compile_tests/props_safety.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn main() {} + +fn app(cx: Scope) -> Element { + let count: &RefCell> = cx.use_hook(|| RefCell::new(Vec::new())); + + render! { + unsafe_child_component { + borrowed: count + } + } +} + +#[derive(Props)] +struct Testing<'a> { + borrowed: &'a RefCell>>, +} + +fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> { + let Testing { borrowed } = cx.props; + let borrowed_temporary_data = + cx.use_hook(|| String::from("This data is only valid for the lifetime of the child")); + + borrowed + .borrow_mut() + .push(render! {"{borrowed_temporary_data}"}); + + cx.render(rsx! { + div { "Hello, world!" } + }) +} diff --git a/packages/core/compile_tests/props_safety.stderr b/packages/core/compile_tests/props_safety.stderr new file mode 100644 index 000000000..565d7b6cc --- /dev/null +++ b/packages/core/compile_tests/props_safety.stderr @@ -0,0 +1,20 @@ +error[E0521]: borrowed data escapes outside of function + --> compile_tests/props_safety.rs:8:5 + | +5 | fn app(cx: Scope) -> Element { + | -- + | | + | `cx` is a reference that is only valid in the function body + | has type `&'1 Scoped<'1>` +... +8 | / render! { +9 | | unsafe_child_component { +10 | | borrowed: count +11 | | } +12 | | } + | | ^ + | | | + | |_____`cx` escapes the function body here + | argument requires that `'1` must outlive `'static` + | + = note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 834b0cabd..05f5cbea9 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -73,3 +73,9 @@ impl EmptyBuilder { pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder { T::builder() } + +#[test] +fn unsafe_props_fail() { + let t = trybuild::TestCases::new(); + t.compile_fail("compile_tests/props_safety.rs"); +} From 80ac1a99afca6c465e16b37a658169edb387525e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 9 Aug 2023 14:20:46 -0700 Subject: [PATCH 47/85] fix miri --- examples/props_safety.rs | 68 --------------------------------- packages/core/src/properties.rs | 1 + 2 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 examples/props_safety.rs diff --git a/examples/props_safety.rs b/examples/props_safety.rs deleted file mode 100644 index 7802e75c9..000000000 --- a/examples/props_safety.rs +++ /dev/null @@ -1,68 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus_desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - let count: &RefCell> = cx.use_hook(|| RefCell::new(Vec::new())); - - let element = render! { - div { - "hello world!" - } - }; - - let nested_borrows = cx.use_hook(Vec::new); - nested_borrows.push("hello world!"); - - cx.render(rsx! { - // unsafe_child_component { - // borrowed: count - // } - safe_child_component { - borrowed: element - } - safe_nested_borrows { - borrowed: &**nested_borrows - } - }) -} - -#[derive(Props)] -struct NestedBorrows<'a> { - borrowed: &'a [&'a str], -} - -fn safe_nested_borrows<'a>(cx: Scope<'a, NestedBorrows<'a>>) -> Element<'a> { - render! { - div { "{cx.props.borrowed:?}" } - } -} - -#[derive(Props)] -struct Testing<'a> { - borrowed: &'a RefCell>>, -} - -fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> { - let Testing { borrowed } = cx.props; - let borrowed = borrowed.borrow(); - cx.render(rsx! { - div { "Hello, world!" } - }) -} - -#[derive(Props)] -struct SafeTesting<'a> { - borrowed: Element<'a>, -} - -fn safe_child_component<'a>(cx: Scope<'a, SafeTesting<'a>>) -> Element<'a> { - let SafeTesting { borrowed } = cx.props; - cx.render(rsx! { - div { - borrowed - } - }) -} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 05f5cbea9..f754f8b68 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -74,6 +74,7 @@ pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) T::builder() } +#[cfg(not(miri))] #[test] fn unsafe_props_fail() { let t = trybuild::TestCases::new(); From 2c79b7fd677da776644003afd4f54e78346edaa0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 9 Aug 2023 15:00:57 -0700 Subject: [PATCH 48/85] Update dioxus-core to 0.4.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e6ce4459b..4c420ad3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ version = "0.4.1" # dependencies that are shared across packages [workspace.dependencies] dioxus = { path = "packages/dioxus", version = "0.4.0" } -dioxus-core = { path = "packages/core", version = "0.4.0" } +dioxus-core = { path = "packages/core", version = "0.4.1" } dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } From 18d255deccfa36648af8e1c5c30dd2106ffd6dc4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 10 Aug 2023 17:17:25 -0700 Subject: [PATCH 49/85] add serde feature to signals --- packages/signals/Cargo.toml | 5 +++++ packages/signals/src/rt.rs | 22 ++++++++++++++++++++++ packages/signals/src/signal.rs | 14 ++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 2c24758b5..0017a1545 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -11,8 +11,13 @@ dioxus-core = { 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/src/rt.rs b/packages/signals/src/rt.rs index 0f0b3a8c2..7322d43b4 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -48,6 +48,28 @@ pub struct CopyValue { origin_scope: ScopeId, } +#[cfg(feature = "serde")] +impl serde::Serialize for CopyValue +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result { + self.value.read().serialize(serializer) + } +} + +#[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 CopyValue { /// Create a new CopyValue. The value will be stored in the current component. /// diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 5d696ee17..a5599a1ce 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -118,6 +118,20 @@ 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 { From bdfb3b628578aea3290b43af018589a66e62e98f Mon Sep 17 00:00:00 2001 From: Steven Pecht Date: Sat, 12 Aug 2023 23:34:42 +1000 Subject: [PATCH 50/85] Update tailwind examples to work with multi-page apps (#1339) * Update tailwind example to work with multi-page apps Use the absolute path to `tailwind.css` so the example works with multi-page apps. Without this change, the style disappears on page reload. * Update tailwind Dioxus.toml to use absolute path for tailwind.css --- examples/tailwind/Dioxus.toml | 2 +- examples/tailwind/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml index 67a63e686..b440d6a69 100644 --- a/examples/tailwind/Dioxus.toml +++ b/examples/tailwind/Dioxus.toml @@ -30,7 +30,7 @@ watch_path = ["src", "public"] [web.resource] # CSS style file -style = ["tailwind.css"] +style = ["/tailwind.css"] # Javascript code file script = [] diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md index f2a08eba9..552ab402f 100644 --- a/examples/tailwind/README.md +++ b/examples/tailwind/README.md @@ -81,7 +81,7 @@ watch_path = ["src", "public"] [web.resource] # CSS style file -style = ["tailwind.css"] +style = ["/tailwind.css"] # Javascript code file script = [] From d559fdaeab7a3f69541aae0c61e6d6672f348e9e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 12 Aug 2023 19:19:46 -0500 Subject: [PATCH 51/85] fix borrowed props with temp values --- .../props_safety_temporary_values.rs | 24 +++++++++++++++++++ .../props_safety_temporary_values.stderr | 12 ++++++++++ packages/core/src/properties.rs | 1 + packages/core/src/scopes.rs | 4 +++- 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/core/compile_tests/props_safety_temporary_values.rs create mode 100644 packages/core/compile_tests/props_safety_temporary_values.stderr diff --git a/packages/core/compile_tests/props_safety_temporary_values.rs b/packages/core/compile_tests/props_safety_temporary_values.rs new file mode 100644 index 000000000..c332f63c4 --- /dev/null +++ b/packages/core/compile_tests/props_safety_temporary_values.rs @@ -0,0 +1,24 @@ +use dioxus::prelude::*; + +fn main() {} + +fn app(cx: Scope) -> Element { + let count = vec![1, 2, 3]; + + render! { + unsafe_child_component { + borrowed: &count + } + } +} + +#[derive(Props)] +struct Testing<'a> { + borrowed: &'a Vec, +} + +fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> { + cx.render(rsx! { + div { "{cx.props.borrowed:?}" } + }) +} diff --git a/packages/core/compile_tests/props_safety_temporary_values.stderr b/packages/core/compile_tests/props_safety_temporary_values.stderr new file mode 100644 index 000000000..c1ef443c9 --- /dev/null +++ b/packages/core/compile_tests/props_safety_temporary_values.stderr @@ -0,0 +1,12 @@ +error[E0515]: cannot return value referencing local variable `count` + --> compile_tests/props_safety_temporary_values.rs:8:5 + | +8 | / render! { +9 | | unsafe_child_component { +10 | | borrowed: &count + | | ------ `count` is borrowed here +11 | | } +12 | | } + | |_____^ returns a value referencing data owned by the current function + | + = note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index f754f8b68..3f0d9ef97 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -79,4 +79,5 @@ pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) fn unsafe_props_fail() { let t = trybuild::TestCases::new(); t.compile_fail("compile_tests/props_safety.rs"); + t.compile_fail("compile_tests/props_safety_temporary_values.rs"); } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5ada89069..17e499d62 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -424,7 +424,9 @@ impl<'src> ScopeState { fn_name: &'static str, ) -> DynamicNode<'src> where - P: Properties + 'child, + // The properties must be valid until the next bump frame + P: Properties + 'src, + // The current bump allocator frame must outlive the child's borrowed props 'src: 'child, { let vcomp = VProps::new(component, P::memoize, props); From 1ab5a03aeffa332354080ed4b625c3ccbc9a29c1 Mon Sep 17 00:00:00 2001 From: Steven Pecht Date: Sun, 13 Aug 2023 10:25:17 +1000 Subject: [PATCH 52/85] Use to identify cargo build artifact if present (#1344) --- packages/cli/src/builder.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 3fa8e7818..332490fda 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -93,18 +93,21 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { // [2] Establish the output directory structure let bindgen_outdir = out_dir.join("assets").join("dioxus"); - let release_type = match config.release { - true => "release", - false => "debug", + let build_profile = if config.custom_profile.is_some() { + config.custom_profile.as_ref().unwrap() + } else if config.release { + "release" + } else { + "debug" }; let input_path = match executable { ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir - .join(format!("wasm32-unknown-unknown/{}", release_type)) + .join(format!("wasm32-unknown-unknown/{}", build_profile)) .join(format!("{}.wasm", name)), ExecutableType::Example(name) => target_dir - .join(format!("wasm32-unknown-unknown/{}/examples", release_type)) + .join(format!("wasm32-unknown-unknown/{}/examples", build_profile)) .join(format!("{}.wasm", name)), }; From 2d5a4f82bf645672370b4154caeac2aa81487844 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 13 Aug 2023 08:47:27 -0500 Subject: [PATCH 53/85] Fix the context API in lists --- packages/core/src/diff.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 270c81730..e3ca19c3a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -15,6 +15,7 @@ use DynamicNode::*; impl<'b> VirtualDom { pub(super) fn diff_scope(&mut self, scope: ScopeId) { + self.runtime.scope_stack.borrow_mut().push(scope); let scope_state = &mut self.get_scope(scope).unwrap(); unsafe { // Load the old and new bump arenas @@ -45,6 +46,7 @@ impl<'b> VirtualDom { (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), }; } + self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { From 943c76b1dad62c4fa719c4942ef49ebe0e4e9ef7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 15 Aug 2023 12:19:05 -0500 Subject: [PATCH 54/85] Fix memory leak in core --- packages/core/src/create.rs | 8 ++++++-- packages/core/src/nodes.rs | 14 +++++++------- packages/rsx/src/lib.rs | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 7fbc8a9c8..a61a8cace 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -87,8 +87,12 @@ impl<'b> VirtualDom { } } - // Intialize the root nodes slice - *node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()]; + // Initialize the root nodes slice + { + let mut nodes_mut = node.root_ids.borrow_mut(); + let len = node.template.get().roots.len(); + nodes_mut.resize(len, ElementId::default()); + }; // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a108f3577..2399c16b8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -54,7 +54,7 @@ pub struct VNode<'a> { /// The IDs for the roots of this template - to be used when moving the template around and removing it from /// the actual Dom - pub root_ids: RefCell>, + pub root_ids: RefCell>, /// The dynamic parts of the template pub dynamic_nodes: &'a [DynamicNode<'a>], @@ -65,11 +65,11 @@ pub struct VNode<'a> { impl<'a> VNode<'a> { /// Create a template with no nodes that will be skipped over during diffing - pub fn empty() -> Element<'a> { + pub fn empty(cx: &'a ScopeState) -> Element<'a> { Some(VNode { key: None, parent: None, - root_ids: Default::default(), + root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())), dynamic_nodes: &[], dynamic_attrs: &[], template: Cell::new(Template { @@ -698,7 +698,7 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { impl<'a, 'b> IntoDynNode<'b> for &'a str { fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { DynamicNode::Text(VText { - value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(), + value: cx.bump().alloc_str(self), id: Default::default(), }) } @@ -741,10 +741,10 @@ impl<'a> IntoTemplate<'a> for VNode<'a> { } } impl<'a> IntoTemplate<'a> for Element<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { + fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { match self { - Some(val) => val.into_template(_cx), - _ => VNode::empty().unwrap(), + Some(val) => val.into_template(cx), + _ => VNode::empty(cx).unwrap(), } } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 783dd3322..f52ecad0e 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -231,6 +231,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { // Render and release the mutable borrow on context let roots = quote! { #( #root_printer ),* }; + let root_count = self.roots.len(); let node_printer = &context.dynamic_nodes; let dyn_attr_printer = &context.dynamic_attributes; let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*])); @@ -247,7 +248,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { parent: None, key: #key_tokens, template: std::cell::Cell::new(TEMPLATE), - root_ids: Default::default(), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(), dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), } From b8600d51442ce3343c4b34614fd8b2c6b343503e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 15 Aug 2023 13:11:03 -0500 Subject: [PATCH 55/85] fix tests --- packages/core/src/diff.rs | 8 +++++++- packages/core/tests/fuzzing.rs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 270c81730..2a6425eaf 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -126,7 +126,13 @@ impl<'b> VirtualDom { }); // Make sure the roots get transferred over while we're here - *right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone(); + { + let mut right = right_template.root_ids.borrow_mut(); + right.clear(); + for &element in left_template.root_ids.borrow().iter() { + right.push(element); + } + } let root_ids = right_template.root_ids.borrow(); diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 40bb5dcbf..1523c4e22 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -179,7 +179,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { node_paths: &[&[0]], attr_paths: &[], }), - root_ids: Default::default(), + root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), dynamic_nodes: cx.bump().alloc([cx.component( create_random_element, DepthProps { depth, root: false }, @@ -276,7 +276,7 @@ fn create_random_element(cx: Scope) -> Element { key: None, parent: None, template: Cell::new(template), - root_ids: Default::default(), + root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), dynamic_nodes: { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() From 6876d2da1c7f03be9b28926f2d66516de2cb2d29 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 15 Aug 2023 13:22:06 -0500 Subject: [PATCH 56/85] fix clippy --- docs/guide/examples/readme_expanded.rs | 3 ++- packages/native-core/tests/fuzzing.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/guide/examples/readme_expanded.rs b/docs/guide/examples/readme_expanded.rs index 896d365e5..1ec30e57b 100644 --- a/docs/guide/examples/readme_expanded.rs +++ b/docs/guide/examples/readme_expanded.rs @@ -89,7 +89,8 @@ fn app(cx: Scope) -> Element { key: None, // The static template this node will use. The template is stored in a Cell so it can be replaced with a new template when hot rsx reloading is enabled template: std::cell::Cell::new(TEMPLATE), - root_ids: Default::default(), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump()) + .into(), dynamic_nodes: __cx.bump().alloc([ // The dynamic count text node (dynamic node id 0) __cx.text_node(format_args!("High-Five counter: {0}", count)), diff --git a/packages/native-core/tests/fuzzing.rs b/packages/native-core/tests/fuzzing.rs index 1d1bd881c..4f0a34b70 100644 --- a/packages/native-core/tests/fuzzing.rs +++ b/packages/native-core/tests/fuzzing.rs @@ -187,7 +187,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { node_paths: &[&[0]], attr_paths: &[], }), - root_ids: Default::default(), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(), dynamic_nodes: cx.bump().alloc([cx.component( create_random_element, DepthProps { depth, root: false }, @@ -257,7 +257,8 @@ fn create_random_element(cx: Scope) -> Element { key: None, parent: None, template: Cell::new(template), - root_ids: Default::default(), + root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()) + .into(), dynamic_nodes: { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() From 555f4d583413db983cfe15cf02c959bc5cdf0ad6 Mon Sep 17 00:00:00 2001 From: Daniel Albl <66034631+DanielAlbl@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:27:19 -0600 Subject: [PATCH 57/85] Add 'with' and 'with_mut' to 'use_shared_state' (#1353) --- packages/hooks/src/use_shared_state.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 019d55942..b900b9dc6 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -282,6 +282,20 @@ impl UseSharedState { ), } } + + /// Take a reference to the inner value temporarily and produce a new value + #[cfg_attr(debug_assertions, track_caller)] + #[cfg_attr(debug_assertions, inline(never))] + pub fn with(&self, immutable_callback: impl FnOnce(&T) -> O) -> O { + immutable_callback(&*self.read()) + } + + /// Take a mutable reference to the inner value temporarily and produce a new value + #[cfg_attr(debug_assertions, track_caller)] + #[cfg_attr(debug_assertions, inline(never))] + pub fn with_mut(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O { + mutable_callback(&mut *self.write()) + } } impl Clone for UseSharedState { From 3e24b3d34a10e218bcb20b94554dfc2a7cf54b1f Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 15 Aug 2023 11:41:32 -0700 Subject: [PATCH 58/85] Publish CLI binaries for cargo-binstall (#1358) * add test release pipeline * fix * debug * oops forgot checkout * debug * fix if * blah * fix working dir * fix cargo build path * add publish step * add cache * fix path * use package instead of working dir * fix cache path * fix typo in checksum * fix upload artifact loop * add more comments * add binstall metadata * test simplified action * fix target * remove dispatch * fix permissions * remove mac arm config * update comments * update comment * replace unmaintained actions-rs --- .github/workflows/cli_release.yml | 54 +++++++++++++++++++++++++++++++ packages/cli/Cargo.toml | 6 ++++ 2 files changed, 60 insertions(+) create mode 100644 .github/workflows/cli_release.yml diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml new file mode 100644 index 000000000..c6c2b81e8 --- /dev/null +++ b/.github/workflows/cli_release.yml @@ -0,0 +1,54 @@ +name: Build CLI for Release + +# Will run automatically on every new release +on: + release: + types: [published] + +jobs: + build-and-upload: + permissions: + contents: write + runs-on: ${{ matrix.platform.os }} + strategy: + matrix: + platform: + - { + target: x86_64-pc-windows-msvc, + os: windows-latest, + toolchain: "1.70.0", + } + - { + target: x86_64-apple-darwin, + os: macos-latest, + toolchain: "1.70.0", + } + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-latest, + toolchain: "1.70.0", + } + steps: + - uses: actions/checkout@v3 + - name: Install stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.platform.toolchain }} + targets: ${{ matrix.platform.target }} + + # Setup the Github Actions Cache for the CLI package + - name: Setup cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/cli -> ../../target + + # This neat action can build and upload the binary in one go! + - name: Build and upload binary + uses: taiki-e/upload-rust-binary-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target: ${{ matrix.platform.target }} + bin: dx + archive: dx-${{ matrix.platform.target }} + checksum: sha256 + manifest_path: packages/cli/Cargo.toml diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 892399a9d..a37798eee 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -100,3 +100,9 @@ name = "dx" [dev-dependencies] tempfile = "3.3" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" From bcebaafb5db7baab6823b41f9ff921f4b8a40f3f Mon Sep 17 00:00:00 2001 From: Dillon Roller Date: Wed, 16 Aug 2023 18:04:58 -0400 Subject: [PATCH 59/85] Update README.md to use 0.4 version of guide (#1370) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e4408d8..5b6a99ad3 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ | Examples | - Guide + Guide | 中文 | From 8946944d68f592d5c1f089dd6cc6ed15c012945e Mon Sep 17 00:00:00 2001 From: Vinicius Gobbo Antunes de Oliveira Date: Thu, 17 Aug 2023 14:57:09 +0200 Subject: [PATCH 60/85] DOC: Add note about form prevent_default behavior. (#1343) * Add note about form prevent_default behavior. * Make the default behavior clear on forms with attached event handlers. --- docs/guide/src/en/interactivity/event_handlers.md | 2 ++ docs/guide/src/pt-br/interactivity/event_handlers.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/guide/src/en/interactivity/event_handlers.md b/docs/guide/src/en/interactivity/event_handlers.md index 4c77cef1e..4e332adf9 100644 --- a/docs/guide/src/en/interactivity/event_handlers.md +++ b/docs/guide/src/en/interactivity/event_handlers.md @@ -51,6 +51,8 @@ Any event handlers will still be called. > Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does _not_ currently support this behavior. Note: this means you cannot conditionally prevent default behavior based on the data in the event. +> Note about forms: if an event handler is attached to the `onsubmit` event of a form, default behavior is to **not submit it**, meaning having `prevent_default: "onsubmit"` will submit it in this case. + ## Handler Props Sometimes, you might want to make a component that accepts an event handler. A simple example would be a `FancyButton` component, which accepts an `on_click` handler: diff --git a/docs/guide/src/pt-br/interactivity/event_handlers.md b/docs/guide/src/pt-br/interactivity/event_handlers.md index 12aad321e..8b3d757ef 100644 --- a/docs/guide/src/pt-br/interactivity/event_handlers.md +++ b/docs/guide/src/pt-br/interactivity/event_handlers.md @@ -64,3 +64,5 @@ Então, você pode usá-lo como qualquer outro manipulador: > Nota: assim como qualquer outro atributo, você pode nomear os manipuladores como quiser! Embora eles devam começar com `on`, para que o prop seja automaticamente transformado em um `EventHandler` no local da chamada. > > Você também pode colocar dados personalizados no evento, em vez de, por exemplo, `MouseData` + +> Nota sobre formulários: se um manipulador de evento está anexado ao evento `onsubmit` em um formulário, o comportamento padrão é de **não submetê-lo**. Portanto, especificar `prevent_default: "onsubmit"` irá submetê-lo. \ No newline at end of file From fa39408d582d8efd63ca780c044b134d38bcf542 Mon Sep 17 00:00:00 2001 From: Vinicius Gobbo Antunes de Oliveira Date: Thu, 17 Aug 2023 14:58:56 +0200 Subject: [PATCH 61/85] If mkcert not enabled, require cert_path and key_path to be specified or error. (#1342) --- packages/cli/src/server/web/mod.rs | 123 +++++++++++++++-------------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index e08d2297a..78eddb574 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -5,7 +5,7 @@ use crate::{ output::{print_console_info, PrettierOptions, WebServerInfo}, setup_file_watcher, setup_file_watcher_hot_reload, }, - BuildResult, CrateConfig, Result, + BuildResult, CrateConfig, Result, WebHttpsConfig, }; use axum::{ body::{Full, HttpBody}, @@ -218,67 +218,12 @@ async fn get_rustls(config: &CrateConfig) -> Result> { return Ok(None); } - let (cert_path, key_path) = match web_config.mkcert { + let (cert_path, key_path) = if let Some(true) = web_config.mkcert { // mkcert, use it - Some(true) => { - // Get paths to store certs, otherwise use ssl/item.pem - let key_path = web_config - .key_path - .clone() - .unwrap_or(DEFAULT_KEY_PATH.to_string()); - - let cert_path = web_config - .cert_path - .clone() - .unwrap_or(DEFAULT_CERT_PATH.to_string()); - - // Create ssl directory if using defaults - if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { - _ = fs::create_dir("ssl"); - } - - let cmd = Command::new("mkcert") - .args([ - "-install", - "-key-file", - &key_path, - "-cert-file", - &cert_path, - "localhost", - "::1", - "127.0.0.1", - ]) - .spawn(); - - match cmd { - Err(e) => { - match e.kind() { - io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."), - e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()), - }; - return Err("failed to generate mkcert certificates".into()); - } - Ok(mut cmd) => { - cmd.wait()?; - } - } - - (cert_path, key_path) - } - // not mkcert - Some(false) => { - // get paths to cert & key - if let (Some(key), Some(cert)) = - (web_config.key_path.clone(), web_config.cert_path.clone()) - { - (cert, key) - } else { - // missing cert or key - return Err("https is enabled but cert or key path is missing".into()); - } - } - // other - _ => return Ok(None), + get_rustls_with_mkcert(web_config)? + } else { + // if mkcert not specified or false, don't use it + get_rustls_without_mkcert(web_config)? }; Ok(Some( @@ -286,6 +231,62 @@ async fn get_rustls(config: &CrateConfig) -> Result> { )) } +fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> { + // Get paths to store certs, otherwise use ssl/item.pem + let key_path = web_config + .key_path + .clone() + .unwrap_or(DEFAULT_KEY_PATH.to_string()); + + let cert_path = web_config + .cert_path + .clone() + .unwrap_or(DEFAULT_CERT_PATH.to_string()); + + // Create ssl directory if using defaults + if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { + _ = fs::create_dir("ssl"); + } + + let cmd = Command::new("mkcert") + .args([ + "-install", + "-key-file", + &key_path, + "-cert-file", + &cert_path, + "localhost", + "::1", + "127.0.0.1", + ]) + .spawn(); + + match cmd { + Err(e) => { + match e.kind() { + io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."), + e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()), + }; + return Err("failed to generate mkcert certificates".into()); + } + Ok(mut cmd) => { + cmd.wait()?; + } + } + + Ok((cert_path, key_path)) +} + +fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> { + // get paths to cert & key + if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) { + Ok((cert, key)) + } else { + // missing cert or key + Err("https is enabled but cert or key path is missing".into()) + } +} + /// Sets up and returns a router async fn setup_router( config: CrateConfig, From f652474a6e163a1ce543d83da02367cbfd88a193 Mon Sep 17 00:00:00 2001 From: wooden-worm <93303706+wooden-worm@users.noreply.github.com> Date: Thu, 17 Aug 2023 06:03:56 -0700 Subject: [PATCH 62/85] FileEngine: expose native file types on different platforms (#1258) * Get native file * Correct visibility --- packages/html/src/events/form.rs | 11 ++++++++- .../src/native_bind/native_file_engine.rs | 6 +++++ packages/web/src/file_engine.rs | 23 +++++++++++++++++++ packages/web/src/lib.rs | 1 + 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 1fe59a640..fab13482b 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Debug}; +use std::{any::Any, collections::HashMap, fmt::Debug}; use dioxus_core::Event; @@ -45,6 +45,12 @@ impl FileEngine for SerializedFileEngine { .await .map(|bytes| String::from_utf8_lossy(&bytes).to_string()) } + + async fn get_native_file(&self, file: &str) -> Option> { + self.read_file(file) + .await + .map(|val| Box::new(val) as Box) + } } #[cfg(feature = "serialize")] @@ -89,6 +95,9 @@ pub trait FileEngine { // read a file to string async fn read_file_to_string(&self, file: &str) -> Option; + + // returns a file in platform's native representation + async fn get_native_file(&self, file: &str) -> Option>; } impl_event! { diff --git a/packages/html/src/native_bind/native_file_engine.rs b/packages/html/src/native_bind/native_file_engine.rs index e3291fa36..88b996a4d 100644 --- a/packages/html/src/native_bind/native_file_engine.rs +++ b/packages/html/src/native_bind/native_file_engine.rs @@ -1,3 +1,4 @@ +use std::any::Any; use std::path::PathBuf; use crate::FileEngine; @@ -40,4 +41,9 @@ impl FileEngine for NativeFileEngine { Some(contents) } + + async fn get_native_file(&self, file: &str) -> Option> { + let file = File::open(file).await.ok()?; + Some(Box::new(file)) + } } diff --git a/packages/web/src/file_engine.rs b/packages/web/src/file_engine.rs index 6552be084..f98f726e2 100644 --- a/packages/web/src/file_engine.rs +++ b/packages/web/src/file_engine.rs @@ -1,3 +1,5 @@ +use std::any::Any; + use dioxus_html::FileEngine; use futures_channel::oneshot; use js_sys::Uint8Array; @@ -100,4 +102,25 @@ impl FileEngine for WebFileEngine { None } } + + async fn get_native_file(&self, file: &str) -> Option> { + let file = self.find(file)?; + Some(Box::new(file)) + } +} + +/// Helper trait for WebFileEngine +#[async_trait::async_trait(?Send)] +pub trait WebFileEngineExt { + /// returns web_sys::File + async fn get_web_file(&self, file: &str) -> Option; +} + +#[async_trait::async_trait(?Send)] +impl WebFileEngineExt for std::sync::Arc { + async fn get_web_file(&self, file: &str) -> Option { + let native_file = self.get_native_file(file).await?; + let ret = native_file.downcast::().ok()?; + Some(*ret) + } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8bea4a460..261232607 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,6 +54,7 @@ // - Do DOM work in the next requestAnimationFrame callback pub use crate::cfg::Config; +pub use crate::file_engine::WebFileEngineExt; use dioxus_core::{Element, Scope, VirtualDom}; use futures_util::{ future::{select, Either}, From d8704a0447ae1376cee18cf6cd556dc0e157eb4a Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Mon, 21 Aug 2023 21:28:32 +0700 Subject: [PATCH 63/85] deps: Update keyboard-types to 0.7. (#1378) --- packages/html/Cargo.toml | 2 +- packages/native-core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 86ccb029d..5644b440c 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -17,7 +17,7 @@ serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } euclid = "0.22.7" enumset = "1.0.11" -keyboard-types = "0.6.2" +keyboard-types = "0.7" async-trait = "0.1.58" serde-value = "0.7.0" tokio = { workspace = true, features = ["fs", "io-util"], optional = true } diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index 5f084732d..6f3f5aa04 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -12,7 +12,7 @@ authors = ["Jonathan Kelley", "Evan Almloff"] [dependencies] dioxus-core = { workspace = true, optional = true } -keyboard-types = "0.6.2" +keyboard-types = "0.7" smallvec = "1.6" rustc-hash = { workspace = true } anymap = "1.0.0-beta.2" From 9c03d68d888453c96be7f1c5ad68b2ec16d64756 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 21 Aug 2023 14:33:52 -0500 Subject: [PATCH 64/85] expose new_in_scope for signals --- packages/signals/src/signal.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index a5599a1ce..f41624f96 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -145,6 +145,21 @@ impl Signal { } } + /// Create a new signal with a custom owner scope. The signal will be dropped when the owner scope is dropped instead of the current scope. + pub fn new_in_scope(value: T, owner: ScopeId) -> Self { + Self { + inner: CopyValue::new_in_scope( + SignalData { + subscribers: Default::default(), + effect_subscribers: Default::default(), + update_any: schedule_update_any().expect("in a virtual dom"), + value, + }, + owner, + ), + } + } + /// Get the scope the signal was created in. pub fn origin_scope(&self) -> ScopeId { self.inner.origin_scope() From 882c3c9dfb6abd061263bbaeaf162633a315762a Mon Sep 17 00:00:00 2001 From: Yolo <92775570+HashCookie@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:39:22 +0800 Subject: [PATCH 65/85] Update ZH_CN.md to use 0.4 version of guide (#1388) * fix: readme awesome link * Update ZH_CN.md to use 0.4 version of guide --- notes/README/ZH_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 1c4abc667..45bf82f09 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -40,7 +40,7 @@ | 代码示例 | - 开发指南 + 开发指南 | English | From da4794d7937ae9d554a1be7e2d7b5dfe6645e80d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 22 Aug 2023 11:56:13 -0700 Subject: [PATCH 66/85] bump core to 0.4.2 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5b55f4628..252e9c83e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,12 +50,12 @@ members = [ exclude = ["examples/mobile_demo"] [workspace.package] -version = "0.4.1" +version = "0.4.2" # dependencies that are shared across packages [workspace.dependencies] dioxus = { path = "packages/dioxus", version = "0.4.0" } -dioxus-core = { path = "packages/core", version = "0.4.1" } +dioxus-core = { path = "packages/core", version = "0.4.2" } dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } From de87ba6548adbf5af08a3148da50e5ea07668083 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 22 Aug 2023 20:48:03 -0400 Subject: [PATCH 67/85] Update all uncommented commands to remove actions-rs (#1366) * Update all uncommented commands to remove actions-rs (#1) * install target in toolchain action * add linux target explicitly * try latest * fix install --- .github/workflows/main.yml | 46 ++++++++-------- .github/workflows/playwright.yml | 71 ++++++++++++------------- packages/cli/.github/workflows/main.yml | 34 ++++-------- 3 files changed, 65 insertions(+), 86 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c6e14ab6f..04f367b8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,52 +85,56 @@ jobs: matrix_test: runs-on: ${{ matrix.platform.os }} + env: + RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} strategy: matrix: platform: - { target: x86_64-pc-windows-msvc, os: windows-latest, - toolchain: '1.70.0', + toolchain: "1.70.0", cross: false, - command: 'test', - args: '--all --tests' + command: "test", + args: "--all --tests", } - { target: x86_64-apple-darwin, os: macos-latest, - toolchain: '1.70.0', + toolchain: "1.70.0", cross: false, - command: 'test', - args: '--all --tests' + command: "test", + args: "--all --tests", } - { target: aarch64-apple-ios, os: macos-latest, - toolchain: '1.70.0', + toolchain: "1.70.0", cross: false, - command: 'build', - args: '--package dioxus-mobile' + command: "build", + args: "--package dioxus-mobile", } - { target: aarch64-linux-android, os: ubuntu-latest, - toolchain: '1.70.0', + toolchain: "1.70.0", cross: true, - command: 'build', - args: '--package dioxus-mobile' + command: "build", + args: "--package dioxus-mobile", } steps: - uses: actions/checkout@v3 - name: install stable - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.platform.toolchain }} - target: ${{ matrix.platform.target }} - override: true - default: true + targets: ${{ matrix.platform.target }} + + - name: Install cross + if: ${{ matrix.platform.cross == true }} + uses: taiki-e/install-action@cross - uses: Swatinem/rust-cache@v2 with: @@ -138,13 +142,8 @@ jobs: save-if: ${{ matrix.features.key == 'all' }} - name: test - uses: actions-rs/cargo@v1 - with: - use-cross: ${{ matrix.platform.cross }} - command: ${{ matrix.platform.command }} - args: --target ${{ matrix.platform.target }} ${{ matrix.platform.args }} - - + run: | + ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} # Coverage is disabled until we can fix it # coverage: @@ -166,4 +165,3 @@ jobs: # uses: codecov/codecov-action@v2 # with: # fail_ci_if_error: false - diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 41fa92fa7..dba32d257 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,9 +1,9 @@ name: Playwright Tests on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] defaults: run: working-directory: ./playwright-tests @@ -16,39 +16,36 @@ jobs: test: if: github.event.pull_request.draft == false timeout-minutes: 60 - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - # Do our best to cache the toolchain and node install steps - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: Swatinem/rust-cache@v2 - - name: Install WASM toolchain - run: rustup target add wasm32-unknown-unknown - - name: Install dependencies - run: npm ci - - name: Install Playwright - run: npm install -D @playwright/test - - name: Install Playwright Browsers - run: npx playwright install --with-deps - # Cache the CLI by using cargo run internally - # - name: Install Dioxus CLI - # uses: actions-rs/cargo@v1 - # with: - # command: install - # args: --path packages/cli - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + # Do our best to cache the toolchain and node install steps + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: Install dependencies + run: npm ci + - name: Install Playwright + run: npm install -D @playwright/test + - name: Install Playwright Browsers + run: npx playwright install --with-deps + # Cache the CLI by using cargo run internally + # - name: Install Dioxus CLI + # uses: actions-rs/cargo@v1 + # with: + # command: install + # args: --path packages/cli + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/packages/cli/.github/workflows/main.yml b/packages/cli/.github/workflows/main.yml index f978ba7a2..b22f78eae 100644 --- a/packages/cli/.github/workflows/main.yml +++ b/packages/cli/.github/workflows/main.yml @@ -8,47 +8,31 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/cargo@v1 - with: - command: check + - run: cargo check test: name: Test Suite runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/cargo@v1 - with: - command: test + - run: cargo test fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v1 - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - run: cargo fmt --all -- --check # clippy: # name: Clippy From 31f8bab20d818506c0cfd902887e66e23515c6d2 Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 25 Aug 2023 16:31:23 +0300 Subject: [PATCH 68/85] Add ScopeId::ROOT (#1398) * add ScopeId::ROOT * replace ScopeId(0) with ScopeId::ROOT --------- Co-authored-by: Jani Mustonen --- examples/mobile_demo/src/lib.rs | 2 +- packages/core/src/arena.rs | 4 +-- packages/core/src/events.rs | 2 +- packages/core/src/scope_context.rs | 4 +-- packages/core/src/scopes.rs | 15 ++++++++ packages/core/src/virtual_dom.rs | 10 +++--- packages/core/tests/attr_cleanup.rs | 8 ++--- packages/core/tests/bubble_error.rs | 2 +- packages/core/tests/context_api.rs | 6 ++-- packages/core/tests/cycle.rs | 6 ++-- packages/core/tests/diff_component.rs | 6 ++-- packages/core/tests/diff_element.rs | 14 ++++---- packages/core/tests/diff_keyed_list.rs | 24 ++++++------- packages/core/tests/diff_unkeyed_list.rs | 36 +++++++++---------- packages/core/tests/lifecycle.rs | 16 ++++----- packages/core/tests/miri_simple.rs | 12 +++---- packages/core/tests/miri_stress.rs | 16 ++++----- .../src/utils/persistant_iterator.rs | 4 +-- packages/signals/tests/create.rs | 2 +- packages/signals/tests/selector.rs | 4 +-- packages/signals/tests/subscribe.rs | 2 +- packages/ssr/src/renderer.rs | 2 +- 22 files changed, 106 insertions(+), 91 deletions(-) diff --git a/examples/mobile_demo/src/lib.rs b/examples/mobile_demo/src/lib.rs index 9d533e52d..992c53541 100644 --- a/examples/mobile_demo/src/lib.rs +++ b/examples/mobile_demo/src/lib.rs @@ -72,7 +72,7 @@ fn app(cx: Scope) -> Element { onclick: move|_| { println!("Clicked!"); items.push(items.len()); - cx.needs_update_any(ScopeId(0)); + cx.needs_update_any(ScopeId::ROOT); println!("Requested update"); }, "Add item" diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c8f7ce3c9..b7f768755 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -35,7 +35,7 @@ impl ElementRef { Self { template: None, path: ElementPath::Root(0), - scope: ScopeId(0), + scope: ScopeId::ROOT, } } } @@ -60,7 +60,7 @@ 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)); + let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT); entry.insert(ElementRef { // We know this is non-null because it comes from a reference diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index b85e0394d..4778ff4d3 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -143,7 +143,7 @@ pub struct EventHandler<'bump, T = ()> { impl Default for EventHandler<'_, T> { fn default() -> Self { Self { - origin: ScopeId(0), + origin: ScopeId::ROOT, callback: Default::default(), } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 14366eefb..173f43e18 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -179,7 +179,7 @@ impl ScopeContext { pub fn provide_root_context(&self, context: T) -> T { with_runtime(|runtime| { runtime - .get_context(ScopeId(0)) + .get_context(ScopeId::ROOT) .unwrap() .provide_context(context) }) @@ -203,7 +203,7 @@ impl ScopeContext { /// 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); + let id = self.tasks.spawn(ScopeId::ROOT, fut); // wake up the scheduler if it is sleeping self.tasks diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 17e499d62..996529a0d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -63,6 +63,21 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> { #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); +impl ScopeId { + /// The root ScopeId. + /// + /// This scope will last for the entire duration of your app, making it convenient for long-lived state + /// that is created dynamically somewhere down the component tree. + /// + /// # Example + /// + /// ```rust, ignore + /// use dioxus_signals::*; + /// let my_persistent_state = Signal::new_in_scope(ScopeId::ROOT, String::new()); + /// ``` + pub const ROOT: ScopeId = ScopeId(0); +} + /// A component's state separate from its props. /// /// This struct exists to provide a common interface for all scopes without relying on generics. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a1def71d2..c2e56c776 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -270,7 +270,7 @@ impl VirtualDom { ); // Unlike react, we provide a default error boundary that just renders the error as a string - root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); + root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(ElementRef::none()); @@ -289,7 +289,7 @@ impl VirtualDom { /// /// This scope has a ScopeId of 0 and is the root of the tree pub fn base_scope(&self) -> &ScopeState { - self.get_scope(ScopeId(0)).unwrap() + self.get_scope(ScopeId::ROOT).unwrap() } /// Build the virtualdom with a global context inserted into the base scope @@ -547,10 +547,10 @@ impl VirtualDom { /// ``` pub fn rebuild(&mut self) -> Mutations { let _runtime = RuntimeGuard::new(self.runtime.clone()); - match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { + match unsafe { self.run_scope(ScopeId::ROOT).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root RenderReturn::Ready(node) => { - let m = self.create_scope(ScopeId(0), node); + let m = self.create_scope(ScopeId::ROOT, node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), m, @@ -663,6 +663,6 @@ impl VirtualDom { impl Drop for VirtualDom { fn drop(&mut self) { // Simply drop this scope which drops all of its children - self.drop_scope(ScopeId(0), true); + self.drop_scope(ScopeId::ROOT, true); } } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 683f02260..539a0b399 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -34,7 +34,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -56,7 +56,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -65,7 +65,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -88,7 +88,7 @@ fn attrs_cycle() { ); // we take the node taken by attributes since we reused it - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index 7d6966c46..af2a34219 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -24,7 +24,7 @@ fn bubbles_error() { let _edits = dom.rebuild().santize(); } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs index 696810cb0..05c66a41e 100644 --- a/packages/core/tests/context_api.rs +++ b/packages/core/tests/context_api.rs @@ -27,11 +27,11 @@ fn state_shares() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 1); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 2); @@ -41,7 +41,7 @@ fn state_shares() { [SetText { value: "Value is 2", id: ElementId(1,) },] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); dom.mark_dirty(ScopeId(2)); let edits = dom.render_immediate(); assert_eq!( diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 10498f5cc..20d1c9343 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -23,7 +23,7 @@ fn cycling_elements() { ); } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -33,7 +33,7 @@ fn cycling_elements() { ); // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -42,7 +42,7 @@ fn cycling_elements() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs index 69ab6de6d..4766ad378 100644 --- a/packages/core/tests/diff_component.rs +++ b/packages/core/tests/diff_component.rs @@ -75,7 +75,7 @@ fn component_swap() { ); } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -84,7 +84,7 @@ fn component_swap() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -93,7 +93,7 @@ fn component_swap() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 3ab9b284a..a0b9bfa84 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -12,19 +12,19 @@ fn text_diff() { let mut vdom = VirtualDom::new(app); _ = vdom.rebuild(); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 1", id: ElementId(2) }] ); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 2", id: ElementId(2) }] ); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 3", id: ElementId(2) }] @@ -46,7 +46,7 @@ fn element_swap() { let mut vdom = VirtualDom::new(app); _ = vdom.rebuild(); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -55,7 +55,7 @@ fn element_swap() { ] ); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -64,7 +64,7 @@ fn element_swap() { ] ); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -73,7 +73,7 @@ fn element_swap() { ] ); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); assert_eq!( vdom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index be26e00c0..ed14c2d95 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -39,7 +39,7 @@ fn keyed_diffing_out_of_order() { ); } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().edits, [ @@ -64,7 +64,7 @@ fn keyed_diffing_out_of_order_adds() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().edits, [ @@ -90,7 +90,7 @@ fn keyed_diffing_out_of_order_adds_3() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().edits, [ @@ -116,7 +116,7 @@ fn keyed_diffing_out_of_order_adds_4() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().edits, [ @@ -142,7 +142,7 @@ fn keyed_diffing_out_of_order_adds_5() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().edits, [ @@ -167,7 +167,7 @@ fn keyed_diffing_additions() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -192,7 +192,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -222,7 +222,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { _ = dom.rebuild(); // LIS: 4, 5, 6 - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -256,7 +256,7 @@ fn controlled_keyed_diffing_out_of_order() { _ = dom.rebuild(); // LIS: 5, 6 - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -289,7 +289,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -318,7 +318,7 @@ fn remove_list() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -343,7 +343,7 @@ fn no_common_keys() { _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 4934f6080..b5be161ef 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -27,7 +27,7 @@ fn list_creates_one_by_one() { ); // Rendering the first item should replace the placeholder with an element - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -38,7 +38,7 @@ fn list_creates_one_by_one() { ); // Rendering the next item should insert after the previous - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -49,7 +49,7 @@ fn list_creates_one_by_one() { ); // ... and again! - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -60,7 +60,7 @@ fn list_creates_one_by_one() { ); // once more - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -107,14 +107,14 @@ fn removes_one_by_one() { // Remove div(3) // Rendering the first item should replace the placeholder with an element - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }] ); // Remove div(2) - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(4) }] @@ -122,7 +122,7 @@ fn removes_one_by_one() { // Remove div(1) and replace with a placeholder // todo: this should just be a remove with no placeholder - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -133,7 +133,7 @@ fn removes_one_by_one() { // load the 3 and replace the placeholder // todo: this should actually be append to, but replace placeholder is fine for now - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -170,7 +170,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -182,7 +182,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -194,7 +194,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -249,19 +249,19 @@ fn removes_one_by_one_multiroot() { ] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] ); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); assert_eq!( dom.render_immediate().santize().edits, [ @@ -328,7 +328,7 @@ fn remove_many() { } { - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -341,7 +341,7 @@ fn remove_many() { } { - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -360,7 +360,7 @@ fn remove_many() { } { - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -376,7 +376,7 @@ fn remove_many() { } { - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 04f6bc3cf..08b342631 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -58,7 +58,7 @@ fn events_generate() { dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate(); assert_eq!( @@ -107,7 +107,7 @@ fn events_generate() { // ); // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateElement { root: Some(2), tag: "div", children: 0 }, // ReplaceWith { root: Some(1), nodes: vec![2] } @@ -115,7 +115,7 @@ fn events_generate() { // ); // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateTextNode { root: Some(1), text: "Text2" }, // ReplaceWith { root: Some(2), nodes: vec![1] } @@ -124,7 +124,7 @@ fn events_generate() { // // child {} // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateElement { root: Some(2), tag: "h1", children: 0 }, // ReplaceWith { root: Some(1), nodes: vec![2] } @@ -133,7 +133,7 @@ fn events_generate() { // // placeholder // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreatePlaceholder { root: Some(1) }, // ReplaceWith { root: Some(2), nodes: vec![1] } @@ -141,7 +141,7 @@ fn events_generate() { // ); // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateTextNode { root: Some(2), text: "text 3" }, // ReplaceWith { root: Some(1), nodes: vec![2] } @@ -149,7 +149,7 @@ fn events_generate() { // ); // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateTextNode { text: "text 0", root: Some(1) }, // CreateTextNode { text: "text 1", root: Some(3) }, @@ -158,7 +158,7 @@ fn events_generate() { // ); // assert_eq!( -// dom.hard_diff(ScopeId(0)).edits, +// dom.hard_diff(ScopeId::ROOT).edits, // [ // CreateElement { tag: "h1", root: Some(2), children: 0 }, // ReplaceWith { root: Some(1), nodes: vec![2] }, diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs index 6da2c4bf4..2282cb4e2 100644 --- a/packages/core/tests/miri_simple.rs +++ b/packages/core/tests/miri_simple.rs @@ -11,7 +11,7 @@ fn app_drops() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } @@ -31,7 +31,7 @@ fn hooks_drop() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } @@ -58,7 +58,7 @@ fn contexts_drop() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } @@ -77,7 +77,7 @@ fn tasks_drop() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } @@ -91,7 +91,7 @@ fn root_props_drop() { ); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } @@ -121,7 +121,7 @@ fn diffing_drops_old() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 15b7ebf41..ff8d69788 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -62,7 +62,7 @@ fn test_memory_leak() { _ = dom.rebuild(); for _ in 0..5 { - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); _ = dom.render_immediate(); } } @@ -97,13 +97,13 @@ fn memo_works_properly() { _ = dom.rebuild(); // todo!() - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); - // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); + // dom.hard_diff(ScopeId::ROOT); } #[test] diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 5d4902b87..2442af29f 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -354,7 +354,7 @@ fn persist_removes() { // "3" iter2.next(&rdom).id(); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); let update = vdom.render_immediate(); println!("{update:#?}"); dioxus_state.apply_mutations(&mut rdom, update); @@ -419,7 +419,7 @@ fn persist_instertions_before() { // "2" iter.next(&rdom).id(); - vdom.mark_dirty(ScopeId(0)); + vdom.mark_dirty(ScopeId::ROOT); let update = vdom.render_immediate(); dioxus_state.apply_mutations(&mut rdom, update); diff --git a/packages/signals/tests/create.rs b/packages/signals/tests/create.rs index 0681263ea..57d8325af 100644 --- a/packages/signals/tests/create.rs +++ b/packages/signals/tests/create.rs @@ -51,7 +51,7 @@ fn drop_signals() { } let _ = dom.rebuild().santize(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); dom.render_immediate(); fn create_without_cx() -> Signal { diff --git a/packages/signals/tests/selector.rs b/packages/signals/tests/selector.rs index 17eb1c38a..811871e88 100644 --- a/packages/signals/tests/selector.rs +++ b/packages/signals/tests/selector.rs @@ -124,7 +124,7 @@ fn memos_prevents_component_rerun() { } let _ = dom.rebuild().santize(); - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); dom.render_immediate(); { @@ -133,7 +133,7 @@ fn memos_prevents_component_rerun() { assert_eq!(current_counter.effect, 2); } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); dom.render_immediate(); dom.render_immediate(); diff --git a/packages/signals/tests/subscribe.rs b/packages/signals/tests/subscribe.rs index 5c1a99347..e0e7d2072 100644 --- a/packages/signals/tests/subscribe.rs +++ b/packages/signals/tests/subscribe.rs @@ -77,7 +77,7 @@ fn reading_subscribes() { } } - dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId::ROOT); dom.render_immediate(); dom.render_immediate(); diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index f23e654fb..154fad641 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -40,7 +40,7 @@ impl Renderer { } pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result { - self.render_scope(buf, dom, ScopeId(0)) + self.render_scope(buf, dom, ScopeId::ROOT) } pub fn render_scope( From 37eb929d6988c3c762b48cefe244292ffbb69e65 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 25 Aug 2023 15:00:38 -0400 Subject: [PATCH 69/85] Explain features necessary for the `pre_cache_static_routes` function. (#1401) --- packages/router/examples/static_generation.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/router/examples/static_generation.rs b/packages/router/examples/static_generation.rs index 4bdc1df5f..19e7c7341 100644 --- a/packages/router/examples/static_generation.rs +++ b/packages/router/examples/static_generation.rs @@ -30,6 +30,8 @@ async fn main() { .join("\n") ); + // This function is available if you enable the ssr feature + // on the dioxus_router crate. pre_cache_static_routes::( &mut renderer, &DefaultRenderer { From 9ae6b46e20d894e6070004f6c8e0ccb1ba0cd29f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 31 Aug 2023 15:43:26 -0500 Subject: [PATCH 70/85] Forward Wry's devtools, and dox flags --- packages/desktop/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index a85c2e045..4b2b6df9f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -55,7 +55,8 @@ default = ["tokio_runtime", "hot-reload"] tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] -tray = ["wry/tray"] +devtools = ["wry/devtools"] +dox = ["wry/dox"] hot-reload = ["dioxus-hot-reload"] [dev-dependencies] From 392c744ad1e6ec7fe229d60cbc3f0fccb7b2c3d5 Mon Sep 17 00:00:00 2001 From: Jay Graves Date: Fri, 1 Sep 2023 08:55:44 -0600 Subject: [PATCH 71/85] Make incremental a cfg feature. (#1409) * Make incremental a cfg feature. * Make the incremental feature default. --- packages/ssr/Cargo.toml | 6 +++++- packages/ssr/src/lib.rs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index d1f460c8f..1450bd46e 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -17,7 +17,7 @@ rustc-hash = "1.1.0" lru = "0.10.0" log = "0.4.13" http = "0.2.9" -tokio = { version = "1.28", features = ["full"] } +tokio = { version = "1.28", features = ["full"], optional = true } [dev-dependencies] dioxus = { workspace = true } @@ -29,3 +29,7 @@ argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" fs_extra = "1.2.0" + +[features] +default = ["incremental"] +incremental = ["dep:tokio"] diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 0f91e02d9..45e3f59ea 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -3,8 +3,11 @@ mod cache; pub mod config; mod fs_cache; +#[cfg(feature = "incremental")] pub mod incremental; +#[cfg(feature = "incremental")] mod incremental_cfg; + pub mod renderer; pub mod template; From d94e2c104cbea56730469de66ff755f4c6c8eeeb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 1 Sep 2023 10:27:28 -0500 Subject: [PATCH 72/85] pin Tauri dependencies to a minor version --- packages/cli/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index a37798eee..65a05488d 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -78,8 +78,8 @@ toml_edit = "0.19.11" # dioxus-rsx = "0.0.1" # bundling -tauri-bundler = { version = "1.2", features = ["native-tls-vendored"] } -tauri-utils = "1.3" +tauri-bundler = { version = "=1.2.*", features = ["native-tls-vendored"] } +tauri-utils = "1.3.*" dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } From 4511b37da875c5d49ef43fa803f5a2deb9da48b5 Mon Sep 17 00:00:00 2001 From: fanyang Date: Sat, 2 Sep 2023 19:44:31 +0800 Subject: [PATCH 73/85] Fix cli compile error due to tauri bundler adding fields (#1416) New fields for debian settings: - desktop_template For NSIS settings: - custom_language_files - template --- packages/cli/src/config.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 2e499451f..0654dc375 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -381,6 +381,7 @@ impl From for tauri_bundler::DebianSettings { tauri_bundler::DebianSettings { depends: val.depends, files: val.files, + desktop_template: None, } } } @@ -522,6 +523,8 @@ impl From for tauri_bundler::NsisSettings { install_mode: val.install_mode.into(), languages: val.languages, display_language_selector: val.display_language_selector, + custom_language_files: None, + template: None, } } } From f818d27fa6a5d4fd4192a4e38676ce630053ecdc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 2 Sep 2023 07:09:02 -0500 Subject: [PATCH 74/85] only collect the rsx and render macros when formatting --- packages/autofmt/src/collect_macros.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/autofmt/src/collect_macros.rs b/packages/autofmt/src/collect_macros.rs index 10e0d185c..841af1316 100644 --- a/packages/autofmt/src/collect_macros.rs +++ b/packages/autofmt/src/collect_macros.rs @@ -17,7 +17,15 @@ struct MacroCollector<'a, 'b> { impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> { fn visit_macro(&mut self, i: &'b Macro) { - self.macros.push(i); + if let Some("rsx" | "render") = i + .path + .segments + .last() + .map(|i| i.ident.to_string()) + .as_deref() + { + self.macros.push(i) + } } } From 1b977fdce91822418f97fb33d416c295ce77180f Mon Sep 17 00:00:00 2001 From: Stygmates Date: Sat, 2 Sep 2023 17:24:30 +0200 Subject: [PATCH 75/85] Add query segment example and documentation (#1417) * fix a few new clippy lints * Add query segment example * Add trait documentation * Change core package to root package The core package contains the virtual dom implementation ([here](https://github.com/DioxusLabs/dioxus/tree/master/packages/core)). Root package might be a more clear way to refer to the main directory --------- Co-authored-by: Evan Almloff --- Cargo.toml | 1 + examples/query_segments_demo/Cargo.toml | 12 ++++ examples/query_segments_demo/src/main.rs | 73 ++++++++++++++++++++++++ packages/router/src/contexts/router.rs | 34 ++++++----- packages/router/src/routable.rs | 6 +- packages/web/src/dom.rs | 1 + 6 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 examples/query_segments_demo/Cargo.toml create mode 100644 examples/query_segments_demo/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 252e9c83e..54c54ebf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ # Full project examples "examples/tailwind", "examples/PWA-example", + "examples/query_segments_demo", # Playwright tests "playwright-tests/liveview", "playwright-tests/web", diff --git a/examples/query_segments_demo/Cargo.toml b/examples/query_segments_demo/Cargo.toml new file mode 100644 index 000000000..4a7d784c7 --- /dev/null +++ b/examples/query_segments_demo/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "query_segments_demo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = { path = "../../packages/dioxus", version = "*" } +dioxus-router = { path = "../../packages/router", version = "*" } +dioxus-web = { path = "../../packages/web", version = "*" } +form_urlencoded = "1.2.0" diff --git a/examples/query_segments_demo/src/main.rs b/examples/query_segments_demo/src/main.rs new file mode 100644 index 000000000..2348f1fe5 --- /dev/null +++ b/examples/query_segments_demo/src/main.rs @@ -0,0 +1,73 @@ +#![allow(non_snake_case, unused)] +//! Example: Url query segments usage +//! ------------------------------------ +//! +//! This example shows how to access and use multiple query segments present in an url on the web. +//! +//! Run `dx serve` and navigate to `http://localhost:8080/blog?name=John&surname=Doe` +use std::fmt::Display; + +use dioxus::prelude::*; +use dioxus_router::prelude::*; + +// ANCHOR: route +#[derive(Routable, Clone)] +#[rustfmt::skip] +enum Route { + // segments that start with ?: are query segments + #[route("/blog?:query_params")] + BlogPost { + // You must include query segments in child variants + query_params: BlogQuerySegments, + }, +} + +#[derive(Debug, Clone, PartialEq)] +struct BlogQuerySegments { + name: String, + surname: String, +} + +/// The display impl needs to display the query in a way that can be parsed: +impl Display for BlogQuerySegments { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "name={}&surname={}", self.name, self.surname) + } +} + +/// The query segment is anything that implements https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html. You can implement that trait for a struct if you want to parse multiple query parameters. +impl FromQuery for BlogQuerySegments { + fn from_query(query: &str) -> Self { + let mut name = None; + let mut surname = None; + let pairs = form_urlencoded::parse(query.as_bytes()); + pairs.for_each(|(key, value)| { + if key == "name" { + name = Some(value.clone().into()); + } + if key == "surname" { + surname = Some(value.clone().into()); + } + }); + Self { + name: name.unwrap(), + surname: surname.unwrap(), + } + } +} + +#[inline_props] +fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { + render! { + div{"This is your blogpost with a query segment:"} + div{format!("{:?}", query_params)} + } +} + +fn App(cx: Scope) -> Element { + render! { Router::{} } +} + +fn main() { + dioxus_web::launch(App); +} diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index 5a9a3f0c4..b15956991 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -2,7 +2,7 @@ use std::{ any::Any, collections::HashSet, rc::Rc, - sync::{Arc, RwLock, RwLockWriteGuard}, + sync::{Arc, RwLock}, }; use dioxus::prelude::*; @@ -36,7 +36,7 @@ struct MutableRouterState { /// A collection of router data that manages all routing functionality. #[derive(Clone)] pub struct RouterContext { - state: Arc>, + state: Rc>, subscribers: Arc>>, subscriber_update: Arc, @@ -56,7 +56,7 @@ impl RouterContext { R: Clone, ::Err: std::fmt::Display, { - let state = Arc::new(RwLock::new(MutableRouterState { + let state = Rc::new(RefCell::new(MutableRouterState { prefix: Default::default(), history: cfg.take_history(), unresolved_error: None, @@ -105,7 +105,7 @@ impl RouterContext { // set the updater { - let mut state = myself.state.write().unwrap(); + let mut state = myself.state.borrow_mut(); state.history.updater(Arc::new(move || { for &id in subscribers.read().unwrap().iter() { (mark_dirty)(id); @@ -117,20 +117,20 @@ impl RouterContext { } pub(crate) fn route_from_str(&self, route: &str) -> Result, String> { - let state = self.state.read().unwrap(); + let state = self.state.borrow(); state.history.parse_route(route) } /// Check whether there is a previous page to navigate back to. #[must_use] pub fn can_go_back(&self) -> bool { - self.state.read().unwrap().history.can_go_back() + self.state.borrow().history.can_go_back() } /// Check whether there is a future page to navigate forward to. #[must_use] pub fn can_go_forward(&self) -> bool { - self.state.read().unwrap().history.can_go_forward() + self.state.borrow().history.can_go_forward() } /// Go back to the previous location. @@ -138,7 +138,7 @@ impl RouterContext { /// Will fail silently if there is no previous location to go to. pub fn go_back(&self) { { - self.state.write().unwrap().history.go_back(); + self.state.borrow_mut().history.go_back(); } self.change_route(); @@ -149,7 +149,7 @@ impl RouterContext { /// Will fail silently if there is no next location to go to. pub fn go_forward(&self) { { - self.state.write().unwrap().history.go_forward(); + self.state.borrow_mut().history.go_forward(); } self.change_route(); @@ -206,8 +206,7 @@ impl RouterContext { /// The route that is currently active. pub fn current(&self) -> R { self.state - .read() - .unwrap() + .borrow() .history .current_route() .downcast::() @@ -218,7 +217,7 @@ impl RouterContext { /// The route that is currently active. pub fn current_route_string(&self) -> String { - self.any_route_to_string(&*self.state.read().unwrap().history.current_route()) + self.any_route_to_string(&*self.state.borrow().history.current_route()) } pub(crate) fn any_route_to_string(&self, route: &dyn Any) -> String { @@ -243,7 +242,7 @@ impl RouterContext { /// The prefix that is currently active. pub fn prefix(&self) -> Option { - self.state.read().unwrap().prefix.clone() + self.state.borrow().prefix.clone() } fn external(&self, external: String) -> Option { @@ -261,8 +260,8 @@ impl RouterContext { } } - fn state_mut(&self) -> RwLockWriteGuard { - self.state.write().unwrap() + fn state_mut(&self) -> RefMut { + self.state.borrow_mut() } /// Manually subscribe to the current route @@ -283,15 +282,14 @@ impl RouterContext { /// Clear any unresolved errors pub fn clear_error(&self) { - self.state.write().unwrap().unresolved_error = None; + self.state.borrow_mut().unresolved_error = None; self.update_subscribers(); } pub(crate) fn render_error<'a>(&self, cx: Scope<'a>) -> Element<'a> { self.state - .read() - .unwrap() + .borrow() .unresolved_error .as_ref() .and_then(|_| (self.failure_external_navigation)(cx)) diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index cd3dd354d..d9f5b2346 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -22,7 +22,11 @@ impl std::fmt::Display for RouteParseError { } } -/// Something that can be created from a query string +/// Something that can be created from a query string. +/// +/// This trait needs to be implemented if you want to turn a query string into a struct. +/// +/// A working example can be found in the `examples` folder in the root package under `query_segments_demo` pub trait FromQuery { /// Create an instance of `Self` from a query string fn from_query(query: &str) -> Self; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 9d0d837c5..52f2ab1a9 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -392,6 +392,7 @@ fn read_input_to_data(target: Element) -> Rc { .dyn_ref() .and_then(|input: &web_sys::HtmlInputElement| { input.files().and_then(|files| { + #[allow(clippy::arc_with_non_send_sync)] crate::file_engine::WebFileEngine::new(files) .map(|f| std::sync::Arc::new(f) as std::sync::Arc) }) From a0d1e5de3a91dcf66d801ee60a2771859050a3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Esp=C3=ADn?= Date: Sun, 3 Sep 2023 00:51:34 +0200 Subject: [PATCH 76/85] fix: Typo on use_on_unmount (#1421) --- packages/hooks/src/use_on_unmount.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/use_on_unmount.rs b/packages/hooks/src/use_on_unmount.rs index 1fa4449c1..1309ca122 100644 --- a/packages/hooks/src/use_on_unmount.rs +++ b/packages/hooks/src/use_on_unmount.rs @@ -1,4 +1,4 @@ -/// Creats a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect) +/// Creates a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect) /// /// Example: /// ```rust From 94f76946fcd2da9f4eaddd61c4a098cd07e798b2 Mon Sep 17 00:00:00 2001 From: Leonard Date: Sun, 3 Sep 2023 00:59:20 +0200 Subject: [PATCH 77/85] Improve CLI docs (#1404) * Improve CLI docs * Fixed all the typos (hopefully). * Improved the bad English. This encompasses grammar, vocabulary, generally awkward phrases, etc. The result is easier to read and understand, and in some cases shorter. * Made some formatting improvements. This includes making formatting more consistent, but I changed some of it to be better, at least in my opinion. * Removed certain unnecessities, the biggest one being the commands documentation. I think this is unnecessary because of the `dx --help` command. It also needs to be updated every time a command gets added/updated/removed. It doesn't really provide much extra insight than `dx --help`. * Improved plugin documentation. I added a warning to ignore all of it, which is ironic, but the fact is that plugins are probably going to change. But by the time I learned that, I already made some changes to the plugin docs. This includes a better guide on how to get started (although still bad since the plugin system is too messy), and some general changes like the aforementioned English or formatting. * And more. I can't list it all, but I didn't use any destructive changes. The content might be reformatted (to be shorter or easier to understand), but the meaning isn't lost. * Fix grammar in packages/cli/docs/src/creating.md Co-authored-by: Evan Simkowitz * Update README.md * Change config example --------- Co-authored-by: Evan Simkowitz --- packages/cli/Dioxus.toml | 39 ++-- packages/cli/README.md | 56 ++++-- packages/cli/docs/book.toml | 2 +- packages/cli/docs/src/SUMMARY.md | 23 +-- packages/cli/docs/src/cmd/README.md | 30 --- packages/cli/docs/src/cmd/build.md | 56 ------ packages/cli/docs/src/cmd/clean.md | 27 --- packages/cli/docs/src/cmd/serve.md | 70 ------- packages/cli/docs/src/cmd/translate.md | 68 ------- packages/cli/docs/src/configure.md | 181 ++++++++---------- packages/cli/docs/src/creating.md | 20 +- packages/cli/docs/src/installation.md | 15 +- packages/cli/docs/src/introduction.md | 27 ++- packages/cli/docs/src/plugin/README.md | 87 +++++++-- .../cli/docs/src/plugin/interface/command.md | 8 +- .../cli/docs/src/plugin/interface/dirs.md | 19 +- packages/cli/docs/src/plugin/interface/log.md | 22 +-- .../cli/docs/src/plugin/interface/network.md | 15 +- packages/cli/docs/src/plugin/interface/os.md | 6 +- .../cli/docs/src/plugin/interface/path.md | 25 +-- 20 files changed, 308 insertions(+), 488 deletions(-) delete mode 100644 packages/cli/docs/src/cmd/README.md delete mode 100644 packages/cli/docs/src/cmd/build.md delete mode 100644 packages/cli/docs/src/cmd/clean.md delete mode 100644 packages/cli/docs/src/cmd/serve.md delete mode 100644 packages/cli/docs/src/cmd/translate.md diff --git a/packages/cli/Dioxus.toml b/packages/cli/Dioxus.toml index 792a15165..323f17da2 100644 --- a/packages/cli/Dioxus.toml +++ b/packages/cli/Dioxus.toml @@ -1,29 +1,31 @@ [application] -# dioxus project name -name = "dioxus-cli" +# App name +name = "project_name" -# default platfrom -# you can also use `dx serve/build --platform XXX` to use other platform -# value: web | desktop -default_platform = "desktop" +# The Dioxus platform to default to +default_platform = "web" -# Web `build` & `serve` dist path +# `build` & `serve` output path out_dir = "dist" -# resource (static) file folder +# The static resource path asset_dir = "public" [web.app] # HTML title tag content -title = "dioxus | ⛺" +title = "project_name" [web.watcher] -watch_path = ["src"] +# When watcher is triggered, regenerate the `index.html` +reload_html = true -# include `assets` in web platform +# Which files or dirs will be monitored +watch_path = ["src", "public"] + +# Include style or script assets [web.resource] # CSS style file @@ -34,12 +36,13 @@ script = [] [web.resource.dev] -# Javascript code file -# serve: [dev-server] only +# Same as [web.resource], but for development servers + +# CSS style file +style = [] + +# JavaScript files script = [] -[application.tools] - -# use binaryen.wasm-opt for output Wasm file -# binaryen just will trigger in `web` platform -binaryen = { wasm_opt = true } +[[web.proxy]] +backend = "http://localhost:8000/api/" \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md index 3167a6746..fcd9d25b4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,43 +1,69 @@ -

-

📦✨ Dioxus CLI

+
+

📦✨ Dioxus CLI

Tooling to supercharge Dioxus projects

-**dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running. -It handles all build, bundling, development and publishing to simplify web development. +The **dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running. +It handles all building, bundling, development and publishing to simplify development. ## Installation -### Install stable version +### Install the stable version (recommended) + ``` cargo install dioxus-cli ``` -### Install from git repository + +### Install the latest development build through git + +To get the latest bug fixes and features, you can install the development version from git. +However, this is not fully tested. +That means you're probably going to have more bugs despite having the latest bug fixes. + ``` cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli ``` + +This will download the CLI from the master branch, +and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). + ### Install from local folder ``` cargo install --path . --debug ``` +## Get started -## Get Started - -Use `dx create project-name` to initialize a new Dioxus project.
- +Use `dx create project-name` to initialize a new Dioxus project. It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository. -
- Alternatively, you can specify the template path: ``` dx create hello --template gh:dioxuslabs/dioxus-template ``` -## Dioxus Config File +Run `dx --help` for a list of all the available commands. +Furthermore, you can run `dx --help` to get help with a specific command. -Dioxus CLI will use `Dioxus.toml` file to Identify some project info and switch some cli feature. +## Dioxus config file -You can get more configure information from [Dioxus CLI Document](https://dioxuslabs.com/cli/configure.html). +You can use the `Dioxus.toml` file for further configuration. +Some fields are mandatory, but the CLI tool will tell you which ones are missing. +You can create a `Dioxus.toml` with all fields already set using `dx config init project-name`, +or you can use this bare-bones template (only mandatory fields) to get started: +```toml +[application] +name = "project-name" +# Currently supported platforms: web, desktop +default_platform = "web" + +[web.app] +title = "Hello" + +[web.watcher] + +[web.resource.dev] +``` + +The full anatomy of `Dioxus.toml` is shown on the [Dioxus website](https://dioxuslabs.com/learn/0.4/CLI/configure). diff --git a/packages/cli/docs/book.toml b/packages/cli/docs/book.toml index c5c216fe4..a1e57f634 100644 --- a/packages/cli/docs/book.toml +++ b/packages/cli/docs/book.toml @@ -3,4 +3,4 @@ authors = ["YuKun Liu"] language = "en" multilingual = false src = "src" -title = "Dioxus Cli" +title = "Dioxus CLI" diff --git a/packages/cli/docs/src/SUMMARY.md b/packages/cli/docs/src/SUMMARY.md index b5e3b91ae..98a66ae21 100644 --- a/packages/cli/docs/src/SUMMARY.md +++ b/packages/cli/docs/src/SUMMARY.md @@ -2,17 +2,12 @@ - [Introduction](./introduction.md) - [Installation](./installation.md) -- [Create a Project](./creating.md) -- [Configure Project](./configure.md) -- [Commands](./cmd/README.md) - - [Build](./cmd/build.md) - - [Serve](./cmd/serve.md) - - [Clean](./cmd/clean.md) - - [Translate](./cmd/translate.md) -- [Plugin Development](./plugin/README.md) - - [API.Log](./plugin/interface/log.md) - - [API.Command](./plugin/interface/command.md) - - [API.OS](./plugin/interface/os.md) - - [API.Directories](./plugin/interface/dirs.md) - - [API.Network](./plugin/interface/network.md) - - [API.Path](./plugin/interface/path.md) \ No newline at end of file +- [Create a project](./creating.md) +- [Configure a project](./configure.md) +- [Plugin development](./plugin/README.md) + - [API.Log](plugin/interface/log.md) + - [API.Command](plugin/interface/command.md) + - [API.OS](plugin/interface/os.md) + - [API.Directories](plugin/interface/dirs.md) + - [API.Network](plugin/interface/network.md) + - [API.Path](plugin/interface/path.md) \ No newline at end of file diff --git a/packages/cli/docs/src/cmd/README.md b/packages/cli/docs/src/cmd/README.md deleted file mode 100644 index 735794635..000000000 --- a/packages/cli/docs/src/cmd/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Commands - -In this chapter we will introduce all `dioxus-cli` commands. - -> You can also use `dx --help` to get cli help info. - -``` -Build, Bundle & Ship Dioxus Apps - -Usage: dx [OPTIONS] - -Commands: - build Build the Rust WASM app and all of its assets - translate Translate some source file into Dioxus code - serve Build, watch & serve the Rust WASM app and all of its assets - create Init a new project for Dioxus - clean Clean output artifacts - bundle Bundle the Rust desktop app and all of its assets - version Print the version of this extension - fmt Format some rsx - check Check the Rust files in the project for issues - config Dioxus config file controls - help Print this message or the help of the given subcommand(s) - -Options: - -v Enable verbose logging - --bin Specify bin target - -h, --help Print help - -V, --version Print version -``` diff --git a/packages/cli/docs/src/cmd/build.md b/packages/cli/docs/src/cmd/build.md deleted file mode 100644 index 6109d14f1..000000000 --- a/packages/cli/docs/src/cmd/build.md +++ /dev/null @@ -1,56 +0,0 @@ -# Build - -The `dx build` command can help you `pack & build` a dioxus project. - -``` -dioxus-build -Build the Rust WASM app and all of its assets - -USAGE: - dx build [OPTIONS] - -OPTIONS: - --example [default: ""] - --platform [default: "default_platform"] - --release [default: false] - --bin [default: None] -``` - -You can use this command to build a project: - -``` -dx build --release -``` - -## Target platform - -Use the `platform` option to choose your target platform: - -``` -# for desktop project -dx build --platform desktop -``` - -`platform` currently only supports `desktop` & `web`. - -``` -# for web project -dx build --platform web -``` - -## Specify workspace bin - -You can add the `--bin` option to select which crate you want Dioxus to build: - -``` -dx build --bin app -``` - -## Build Example - -You can use the `example` option to select a example to build: - -``` -# build the `test` example -dx build --exmaple test -``` diff --git a/packages/cli/docs/src/cmd/clean.md b/packages/cli/docs/src/cmd/clean.md deleted file mode 100644 index ef938d245..000000000 --- a/packages/cli/docs/src/cmd/clean.md +++ /dev/null @@ -1,27 +0,0 @@ -# Clean - -`dx clean` will clear the build artifacts (the out_dir and the cargo cache) - -``` -dioxus-clean -Clean build artifacts - -USAGE: - dx clean [OPTIONS] - -OPTIONS: - --bin [default: None] -``` - -# Example - -``` -dx clean -``` - -# Specify workspace bin -You can add the `--bin` option to select which crate you want Dioxus to clean artifacts from: - -``` -dx clean --bin app -``` diff --git a/packages/cli/docs/src/cmd/serve.md b/packages/cli/docs/src/cmd/serve.md deleted file mode 100644 index d8b4e0d42..000000000 --- a/packages/cli/docs/src/cmd/serve.md +++ /dev/null @@ -1,70 +0,0 @@ -# Serve - -The `dx serve` can start a dev server with hot-reloading - -``` -dioxus-serve -Build, watch & serve the Rust WASM app and all of its assets - -USAGE: - dx serve [OPTIONS] - -OPTIONS: - --example [default: ""] - --platform [default: "default_platform"] - --release [default: false] - --hot-reload [default: false] - --bin [default: None] -``` - -You can use this command to build project and start a dev server: - -``` -dx serve -``` - -## Serve Example - -You can use the `example` option to serve a example: - -``` -# serve the `test` example -dx serve --exmaple test -``` - -## Specify workspace bin - -You can add the `--bin` option to select which crate you want Dioxus to build and serve: - -``` -dx serve --bin app -``` - -## Open Browser - -You can add the `--open` option to open system default browser when server startup: - -``` -dx serve --open -``` - -## RSX Hot Reloading - -You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile: - -``` -dx serve --open -``` - -## Cross Origin Policy - -You can add the `cross-origin-policy` option to change cross-origin header to: - -``` - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp -``` - -``` -dx serve --corss-origin-policy -``` diff --git a/packages/cli/docs/src/cmd/translate.md b/packages/cli/docs/src/cmd/translate.md deleted file mode 100644 index b03b4422c..000000000 --- a/packages/cli/docs/src/cmd/translate.md +++ /dev/null @@ -1,68 +0,0 @@ -# Translate - -`dx translate` can translate some `html` file into a Dioxus compoent - -``` -dioxus-translate -Translate some source file into a Dioxus component - -USAGE: - dx translate [OPTIONS] [OUTPUT] - -ARGS: - Output file, defaults to stdout if not present - -OPTIONS: - -c, --component Activate debug mode - -f, --file Input file -``` - -## Translate HTML to stdout - -You can use the `file` option to set path to the `html` file to translate: - -``` -dx transtale --file ./index.html -``` - -## Output rsx to a file - -You can pass a file to the traslate command to set the path to write the output of the command to: - -``` -dx translate --file ./index.html component.rsx -``` - -## Output rsx to a file - -Setting the `component` option will create a compoent from the HTML: - -``` -dx translate --file ./index.html --component -``` - -## Example - -This HTML: -```html -
-

Hello World

- Link -
-``` - -Translates into this Dioxus component: - -```rust -fn component(cx: Scope) -> Element { - cx.render(rsx! { - div { - h1 { "Hello World" }, - a { - href: "https://dioxuslabs.com/", - "Link" - } - } - }) -} -``` diff --git a/packages/cli/docs/src/configure.md b/packages/cli/docs/src/configure.md index 9d0cb0a9b..3b9f6189b 100644 --- a/packages/cli/docs/src/configure.md +++ b/packages/cli/docs/src/configure.md @@ -1,166 +1,155 @@ # Configure Project +This chapter will teach you how to configure the CLI with the `Dioxus.toml` file. +There's an [example](#config-example) which has comments to describe individual keys. +You can copy that or view this documentation for a more complete learning experience. -This chapter will introduce you to how to configure the CLI with your `Dioxus.toml` file - -Be aware that if the config file is present in the folder, some fields must be filled out, or the CLI tool will abort. The mandatory [table headers](https://toml.io/en/v1.0.0#table) and keys will have a '✍' sign beside it. +"🔒" indicates a mandatory item. Some headers are mandatory, but none of the keys inside them are. It might look weird, but it's normal. Simply don't include any keys. ## Structure -The CLI uses a `Dioxus.toml` file in the root of your crate to define some configuration for your `dioxus` project. +Each header has it's TOML form directly under it. -### Application ✍ +### Application 🔒 -General application confiration: - -``` +```toml [application] -# configuration ``` -1. ***name*** ✍ - project name & title -2. ***default_platform*** ✍ - which platform target for this project. +Application-wide configuration. Applies to both web and desktop. + +1. **name** 🔒 - Project name & title. + ```toml + name = "my_project" ``` - name = "my-project" - ``` -2. ***default_platform*** - The platform this project targets - ```ß - # current supported platforms: web, desktop - # default: web + +2. **default_platform** 🔒 - The platform this project targets + ```toml + # Currently supported platforms: web, desktop default_platform = "web" ``` - if you change this to `desktop`, the `dx build` will default building a desktop app -3. ***out_dir*** - The directory to place the build artifacts from `dx build` or `dx service` into. This is also where the `assets` directory will be copied to - ``` + +3. **out_dir** - The directory to place the build artifacts from `dx build` or `dx serve` into. This is also where the `assets` directory will be copied into. + ```toml out_dir = "dist" ``` -4. ***asset_dir*** - The directory with your static assets. The CLI will automatically copy these assets into the ***out_dir*** after a build/serve. - ``` + +4. **asset_dir** - The directory with your static assets. The CLI will automatically copy these assets into the **out_dir** after a build/serve. + ```toml asset_dir = "public" ``` -5. ***sub_package*** - The sub package in the workspace to build by default - ``` + +5. **sub_package** - The sub package in the workspace to build by default. + ```toml sub_package = "my-crate" ``` -### Web.App ✍ +### Web.App 🔒 -Configeration specific to web applications: - -``` +```toml [web.app] -# configuration ``` -1. ***title*** - The title of the web page - ``` +Web-specific configuration. + +1. **title** - The title of the web page. + ```toml # HTML title tag content - title = "dioxus app | ⛺" - ``` -2. ***base_path*** - The base path to build the appliation for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on github pages. + title = "project_name" ``` + +2. **base_path** - The base path to build the application for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on GitHub Pages. + ```toml # The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served base_path = "my_application" ``` ### Web.Watcher ✍ -Configeration related to the development server: - -``` +```toml [web.watcher] -# configuration ``` -1. ***reload_html*** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt - ``` +Development server configuration. + +1. **reload_html** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt + ```toml reload_html = true ``` -2. ***watch_path*** - The files & directories to moniter for changes - ``` + +2. **watch_path** - The files & directories to monitor for changes + ```toml watch_path = ["src", "public"] ``` -3. ***index_on_404*** - If enabled, Dioxus CLI will serve the root page when a route is not found. *This is needed when serving an application that uses the router* - ``` + +3. **index_on_404** - If enabled, Dioxus will serve the root page when a route is not found. +*This is needed when serving an application that uses the router*. +However, when serving your app using something else than Dioxus (e.g. GitHub Pages), you will have to check how to configure it on that platform. +In GitHub Pages, you can make a copy of `index.html` named `404.html` in the same directory. + ```toml index_on_404 = true ``` -### Web.Resource ✍ +### Web.Resource 🔒 -Configeration related to static resources your application uses: -``` +```toml [web.resource] -# configuration ``` -1. ***style*** - The styles (`.css` files) to include in your application - ``` +Static resource configuration. + +1. **style** - CSS files to include in your application. + ```toml style = [ - # include from public_dir. + # Include from public_dir. "./assets/style.css", - # or some asset from online cdn. + # Or some asset from online cdn. "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css" ] ``` -2. ***script*** - The additional scripts (`.js` files) to include in your application - ``` - style = [ - # include from public_dir. - "./assets/index.js", - # or some asset from online cdn. + +2. **script** - JavaScript files to include in your application. + ```toml + script = [ + # Include from asset_dir. + "./public/index.js", + # Or from an online CDN. "https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js" ] ``` -### Web.Resource.Dev ✍ +### Web.Resource.Dev 🔒 -Configeration related to static resources your application uses in development: -``` +```toml [web.resource.dev] -# configuration ``` -1. ***style*** - The styles (`.css` files) to include in your application - ``` - style = [ - # include from public_dir. - "./assets/style.css", - # or some asset from online cdn. - "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css" - ] - ``` -2. ***script*** - The additional scripts (`.js` files) to include in your application - ``` - style = [ - # include from public_dir. - "./assets/index.js", - # or some asset from online cdn. - "https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js" - ] - ``` +This is the same as [`[web.resource]`](#webresource-), but it only works in development servers. +For example, if you want to include a file in a `dx serve` server, but not a `dx serve --release` server, put it here. ### Web.Proxy -Configeration related to any proxies your application requires durring development. Proxies will forward requests to a new service - -``` +```toml [[web.proxy]] -# configuration ``` -1. ***backend*** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404 - ``` +Configuration related to any proxies your application requires during development. Proxies will forward requests to a new service. + +1. **backend** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404 + ```toml backend = "http://localhost:8000/api/" ``` - This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is not currently supported). + This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is currently not supported). ## Config example +This includes all fields, mandatory or not. + ```toml [application] -# App (Project) Name -name = "{{project-name}}" +# App name +name = "project_name" # The Dioxus platform to default to default_platform = "web" @@ -168,23 +157,23 @@ default_platform = "web" # `build` & `serve` output path out_dir = "dist" -# the static resource path +# The static resource path asset_dir = "public" [web.app] # HTML title tag content -title = "dioxus | ⛺" +title = "project_name" [web.watcher] -# when watcher is triggered, regenerate the `index.html` +# When watcher is triggered, regenerate the `index.html` reload_html = true -# which files or dirs will be monitored +# Which files or dirs will be monitored watch_path = ["src", "public"] -# include `assets` in web platform +# Include style or script assets [web.resource] # CSS style file @@ -195,12 +184,12 @@ script = [] [web.resource.dev] -# serve: [dev-server] only +# Same as [web.resource], but for development servers # CSS style file style = [] -# Javascript code file +# JavaScript files script = [] [[web.proxy]] diff --git a/packages/cli/docs/src/creating.md b/packages/cli/docs/src/creating.md index 6c2d5754c..4c594dfd8 100644 --- a/packages/cli/docs/src/creating.md +++ b/packages/cli/docs/src/creating.md @@ -1,39 +1,37 @@ # Create a Project -Once you have the Dioxus CLI tool installed, you can use it to create dioxus project. +Once you have the Dioxus CLI installed, you can use it to create your own project! ## Initializing a default project -First, run the `dx create` command to create a new project ready to be used with Dioxus and the Dioxus CLI: - +First, run the `dx create` command to create a new project: ``` dx create hello-dioxus ``` -> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template) -> This default template is use for `web` platform application. +> It will clone this [template](https://github.com/DioxusLabs/dioxus-template). +> This default template is used for `web` platform application. > > You can choose to create your project from a different template by passing the `template` argument: > ``` > dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template > ``` -Next, move the current directory into your new project: +Next, navigate into your new project: ``` cd hello-dioxus ``` -> Make sure `wasm32 target` is installed before running the Web project. -> You can install the wasm target for rust using rustup: +> Make sure the WASM target is installed before running the projects. +> You can install the WASM target for rust using rustup: > ``` > rustup target add wasm32-unknown-unknown > ``` -Finally, create serve your project with the Dioxus CLI: - +Finally, serve your project: ``` dx serve ``` -By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/) +By default, the CLI serves your website at [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/). diff --git a/packages/cli/docs/src/installation.md b/packages/cli/docs/src/installation.md index b4326d296..5030170f4 100644 --- a/packages/cli/docs/src/installation.md +++ b/packages/cli/docs/src/installation.md @@ -1,22 +1,23 @@ # Installation -Choose any one of the methods below to install the Dioxus CLI: +## Install the latest development build through git -## Install from latest git version - -To get the most up to date bug fixes and features of the Dioxus CLI, you can install the development version from git. +To get the latest bug fixes and features, you can install the development version from git. ``` cargo install --git https://github.com/Dioxuslabs/cli ``` -This will automatically download `Dioxus-CLI` source from github master branch, +This will download `Dioxus-CLI` source from GitHub master branch, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). -## Install from `crates.io` version +## Install stable through `crates.io` -The published version of the Dioxus CLI is updated less often, but should be more stable than the git version of the Dioxus CLI. +The published version of the Dioxus CLI is updated less often, but is more stable than the git version. ``` cargo install dioxus-cli ``` + +Run `dx --help` for a list of all the available commands. +Furthermore, you can run `dx --help` to get help with a specific command. \ No newline at end of file diff --git a/packages/cli/docs/src/introduction.md b/packages/cli/docs/src/introduction.md index 2f857a2a5..8b10b0546 100644 --- a/packages/cli/docs/src/introduction.md +++ b/packages/cli/docs/src/introduction.md @@ -1,21 +1,18 @@ # Introduction -📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground. - -![dioxus-logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png) - -It includes `dev server`, `hot reload` and some `quick command` to help you use dioxus. +The 📦✨ **Dioxus CLI** is a tool to help get Dioxus projects off the ground. ## Features +* Build and pack a Dioxus project +* `html` to `rsx` conversion tool +* Hot Reload for `web` platform +* Create a Dioxus project from `git` repo +* And more! + \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/README.md b/packages/cli/docs/src/plugin/README.md index 0a1a7b606..fe7375618 100644 --- a/packages/cli/docs/src/plugin/README.md +++ b/packages/cli/docs/src/plugin/README.md @@ -1,24 +1,85 @@ -# CLI Plugin Development +# CLI Plugin development -> For Cli 0.2.0 we will add `plugin-develop` support. +**IMPORTANT: Ignore this documentation. Plugins are yet to be released and chances are it won't work for you. This is just what plugins *could* look like.** -Before the 0.2.0 we use `dx tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice. +In the past we used `dx tool` to use and install tools, but it was a flawed system. +Tools were hard-coded by us, but people want more tools than we could code, so this plugin system was made to let +anyone develop plugins and use them in Dioxus projects. -### Why Lua ? +Plugin resources: +* [Source code](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli/src/plugin) +* [Unofficial Dioxus plugin community](https://github.com/DioxusPluginCommunity). Contains certain plugins you can use right now. -We choose `Lua: 5.4` to be the plugin develop language, because cli plugin is not complex, just like a workflow, and user & developer can write some easy code for their plugin. We have **vendored** lua in cli program, and user don't need install lua runtime in their computer, and the lua parser & runtime doesn't take up much disk memory. +### Why Lua? -### Event Management +We chose Lua `5.4` to be the plugin developing language, +because it's extremely lightweight, embeddable and easy to learn. +We installed Lua into the CLI, so you don't need to do it yourself. -The plugin library have pre-define some important event you can control: +Lua resources: +* [Official website](https://www.lua.org/). You can basically find everything here. +* [Awesome Lua](https://github.com/LewisJEllis/awesome-lua). Additional resources (such as Lua plugins for your favorite IDE), and other *awesome* tools! -- `build.on_start` -- `build.on_finished` -- `serve.on_start` -- `serve.on_rebuild` -- `serve.on_shutdown` -### Plugin Template +## Creating a plugin + +A plugin is just an `init.lua` file. +You can include other files using `dofile(path)`. +You need to have a plugin and a manager instance, which you can get using `require`: +```lua +local plugin = require("plugin") +local manager = require("manager") +``` + +You need to set some `manager` fields and then initialize the plugin: +```lua +manager.name = "My first plugin" +manager.repository = "https://github.com/john-doe/my-first-plugin" -- The repository URL. +manager.author = "John Doe " +manager.version = "0.1.0" +plugin.init(manager) +``` + +You also need to return the `manager`, which basically represents your plugin: +```lua +-- Your code here. +-- End of file. + +manager.serve.interval = 1000 +return manager +``` + +And you're ready to go. Now, go and have a look at the stuff below and the API documentation. + +### Plugin info + +You will encounter this type in the events below. The keys are as follows: +* `name: string` - The name of the plugin. +* `repository: string` - The plugin repository URL. +* `author: string` - The author of the plugin. +* `version: string` - The plugin version. + +### Event management + +The plugin library has certain events that you can subscribe to. + +* `manager.on_init` - Triggers the first time the plugin is loaded. +* `manager.build.on_start(info)` - Triggers before the build process. E.g., before `dx build`. +* `manager.build.on_finish(info)` - Triggers after the build process. E.g., after `dx build`. +* `manager.serve.on_start(info)` - Triggers before the serving process. E.g., before `dx serve`. +* `manager.serve.on_rebuild_start(info)` - Triggers before the server rebuilds the web with hot reload. +* `manager.serve.on_rebuild_end(info)` - Triggers after the server rebuilds the web with hot reload. +* `manager.serve.on_shutdown` - Triggers when the server is shutdown. E.g., when the `dx serve` process is terminated. + +To subscribe to an event, you simply need to assign it to a function: + +```lua +manager.build.on_start = function (info) + log.info("[plugin] Build starting: " .. info.name) +end +``` + +### Plugin template ```lua package.path = library_dir .. "/?.lua" diff --git a/packages/cli/docs/src/plugin/interface/command.md b/packages/cli/docs/src/plugin/interface/command.md index 18cd32769..2882e5153 100644 --- a/packages/cli/docs/src/plugin/interface/command.md +++ b/packages/cli/docs/src/plugin/interface/command.md @@ -1,15 +1,15 @@ # Command Functions -> you can use command functions to execute some code & script +You can use command functions to execute code and scripts. -Type Define: +Type definition: ``` Stdio: "Inherit" | "Piped" | "Null" ``` ### `exec(commands: [string], stdout: Stdio, stderr: Stdio)` -you can use this function to run some command on the current system. +You can use this function to run some commands on the current system. ```lua local cmd = plugin.command @@ -18,4 +18,4 @@ manager.test = function () cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"}) end ``` -> Warning: This function don't have exception catch. \ No newline at end of file +> Warning: This function doesn't catch exceptions. \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/dirs.md b/packages/cli/docs/src/plugin/interface/dirs.md index 4173c922f..9984802ff 100644 --- a/packages/cli/docs/src/plugin/interface/dirs.md +++ b/packages/cli/docs/src/plugin/interface/dirs.md @@ -1,33 +1,28 @@ # Dirs Functions -> you can use Dirs functions to get some directory path +Dirs functions are for getting various directory paths. Not to be confused with `plugin.path`. +### `plugin_dir() -> string` -### plugin_dir() -> string - -You can get current plugin **root** directory path +Get the plugin's root directory path. ```lua local path = plugin.dirs.plugin_dir() -- example: ~/Development/DioxusCli/plugin/test-plugin/ ``` -### bin_dir() -> string +### `bin_dir() -> string` -You can get plugin **bin** direcotry path - -Sometime you need install some binary file like `tailwind-cli` & `sass-cli` to help your plugin work, then you should put binary file in this directory. +Get the plugin's binary directory path. Put binary files like `tailwind-cli` or `sass-cli` in this directory. ```lua local path = plugin.dirs.bin_dir() -- example: ~/Development/DioxusCli/plugin/test-plugin/bin/ ``` -### temp_dir() -> string +### `temp_dir() -> string` -You can get plugin **temp** direcotry path - -Just put some temporary file in this directory. +Get the plugin's temporary directory path. Put any temporary files here. ```lua local path = plugin.dirs.bin_dir() diff --git a/packages/cli/docs/src/plugin/interface/log.md b/packages/cli/docs/src/plugin/interface/log.md index fcd72595b..ee437b77a 100644 --- a/packages/cli/docs/src/plugin/interface/log.md +++ b/packages/cli/docs/src/plugin/interface/log.md @@ -1,46 +1,46 @@ # Log Functions -> You can use log function to print some useful log info +You can use log functions to print various logging information. -### Trace(info: string) +### `trace(info: string)` -Print trace log info +Print trace log info. ```lua local log = plugin.log log.trace("trace information") ``` -### Debug(info: string) +### `debug(info: string)` -Print debug log info +Print debug log info. ```lua local log = plugin.log log.debug("debug information") ``` -### Info(info: string) +### `info(info: string)` -Print info log info +Print info log info. ```lua local log = plugin.log log.info("info information") ``` -### Warn(info: string) +### `warn(info: string)` -Print warning log info +Print warning log info. ```lua local log = plugin.log log.warn("warn information") ``` -### Error(info: string) +### `error(info: string)` -Print error log info +Print error log info. ```lua local log = plugin.log diff --git a/packages/cli/docs/src/plugin/interface/network.md b/packages/cli/docs/src/plugin/interface/network.md index 358c9c498..566bc5073 100644 --- a/packages/cli/docs/src/plugin/interface/network.md +++ b/packages/cli/docs/src/plugin/interface/network.md @@ -1,12 +1,13 @@ # Network Functions -> you can use Network functions to download & read some data from internet +You can use Network functions to download & read some data from the internet. -### download_file(url: string, path: string) -> boolean +### `download_file(url: string, path: string) -> boolean` -This function can help you download some file from url, and it will return a *boolean* value to check the download status. (true: success | false: fail) +Downloads a file from the specified URL, +and returns a `boolean` that represents the download status (true: success, false: failure). -You need pass a target url and a local path (where you want to save this file) +You need to pass a target URL and a local path (where you want to save this file). ```lua -- this file will download to plugin temp directory @@ -19,9 +20,11 @@ if status != true then end ``` -### clone_repo(url: string, path: string) -> boolean +### `clone_repo(url: string, path: string) -> boolean` -This function can help you use `git clone` command (this system must have been installed git) +Clone a repository from the given URL into the given path. +Returns a `boolean` that represents the clone status (true: success, false: failure). +The system executing this function must have git installed. ```lua local status = plugin.network.clone_repo( diff --git a/packages/cli/docs/src/plugin/interface/os.md b/packages/cli/docs/src/plugin/interface/os.md index 72a17c336..84f4697b8 100644 --- a/packages/cli/docs/src/plugin/interface/os.md +++ b/packages/cli/docs/src/plugin/interface/os.md @@ -1,10 +1,10 @@ # OS Functions -> you can use OS functions to get some system information +OS functions are for getting system information. -### current_platform() -> string ("windows" | "macos" | "linux") +### `current_platform() -> string ("windows" | "macos" | "linux")` -This function can help you get system & platform type: +Get the current OS platform. ```lua local platform = plugin.os.current_platform() diff --git a/packages/cli/docs/src/plugin/interface/path.md b/packages/cli/docs/src/plugin/interface/path.md index ee787ecf9..a5bee1eb9 100644 --- a/packages/cli/docs/src/plugin/interface/path.md +++ b/packages/cli/docs/src/plugin/interface/path.md @@ -1,10 +1,13 @@ # Path Functions -> you can use path functions to operate valid path string +You can use path functions to perform operations on valid path strings. -### join(path: string, extra: string) -> string +### `join(path: string, extra: string) -> string` -This function can help you extend a path, you can extend any path, dirname or filename. + +Extend a path; you can extend both directory and file paths. ```lua local current_path = "~/hello/dioxus" @@ -12,9 +15,9 @@ local new_path = plugin.path.join(current_path, "world") -- new_path = "~/hello/dioxus/world" ``` -### parent(path: string) -> string +### `parent(path: string) -> string` -This function will return `path` parent-path string, back to the parent. +Return the parent path of the specified path. The parent path is always a directory. ```lua local current_path = "~/hello/dioxus" @@ -22,14 +25,14 @@ local new_path = plugin.path.parent(current_path) -- new_path = "~/hello/" ``` -### exists(path: string) -> boolean +### `exists(path: string) -> boolean` -This function can check some path (dir & file) is exists. +Check if the specified path exists, as either a file or a directory. -### is_file(path: string) -> boolean +### `is_file(path: string) -> boolean` -This function can check some path is a exist file. +Check if the specified path is a file. -### is_dir(path: string) -> boolean +### `is_dir(path: string) -> boolean` -This function can check some path is a exist dir. \ No newline at end of file +Check if the specified path is a directory. \ No newline at end of file From 71264b1ee3d0949e4d709b62ab02f522b2ed04ec Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 2 Sep 2023 18:28:44 -0500 Subject: [PATCH 78/85] bump bundler to "=1.3.*" --- packages/cli/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 65a05488d..b2beb2781 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -78,8 +78,8 @@ toml_edit = "0.19.11" # dioxus-rsx = "0.0.1" # bundling -tauri-bundler = { version = "=1.2.*", features = ["native-tls-vendored"] } -tauri-utils = "1.3.*" +tauri-bundler = { version = "=1.3.*", features = ["native-tls-vendored"] } +tauri-utils = "=1.4.*" dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } From 2cfa0e4ea92b769f2f282b8632d56076a87a6b41 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sat, 2 Sep 2023 16:31:34 -0700 Subject: [PATCH 79/85] fix: prettier_build unavailabe (#1410) Co-authored-by: Evan Almloff --- packages/cli/Cargo.toml | 3 +-- packages/cli/src/builder.rs | 12 +++++------- packages/cli/src/cli/build.rs | 2 +- packages/cli/src/server/web/mod.rs | 6 +++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index a37798eee..1d497d84c 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -36,7 +36,7 @@ chrono = "0.4.19" anyhow = "1.0.53" hyper = "0.14.17" hyper-rustls = "0.23.2" -indicatif = "0.17.0-rc.11" +indicatif = "0.17.5" subprocess = "0.2.9" axum = { version = "0.5.1", features = ["ws", "headers"] } @@ -75,7 +75,6 @@ gitignore = "1.0.7" open = "4.1.0" cargo-generate = "0.18" toml_edit = "0.19.11" -# dioxus-rsx = "0.0.1" # bundling tauri-bundler = { version = "1.2", features = ["native-tls-vendored"] } diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 332490fda..7f1b49ace 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -22,7 +22,7 @@ pub struct BuildResult { pub elapsed_time: u128, } -pub fn build(config: &CrateConfig, quiet: bool) -> Result { +pub fn build(config: &CrateConfig) -> Result { // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [2] Generate the appropriate build folders // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm @@ -53,7 +53,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .arg("build") .arg("--target") .arg("wasm32-unknown-unknown") - .arg("--message-format=json"); + .arg("--message-format=json") + .arg("--quiet"); let cmd = if config.release { cmd.arg("--release") @@ -66,8 +67,6 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { cmd }; - let cmd = if quiet { cmd.arg("--quiet") } else { cmd }; - let cmd = if config.custom_profile.is_some() { let custom_profile = config.custom_profile.as_ref().unwrap(); cmd.arg("--profile").arg(custom_profile) @@ -386,10 +385,9 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { } } - StopSpinOnDrop(pb.clone()); - let stdout = cmd.detached().stream_stdout()?; let reader = std::io::BufReader::new(stdout); + for message in cargo_metadata::Message::parse_stream(reader) { match message.unwrap() { Message::CompilerMessage(msg) => { @@ -409,7 +407,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { } } Message::CompilerArtifact(artifact) => { - pb.set_message(format!("Compiling {} ", artifact.package_id)); + pb.set_message(format!("⚙️ Compiling {} ", artifact.package_id)); pb.tick(); } Message::BuildScriptExecuted(script) => { diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 3053a6e5a..d701d2ee8 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -42,7 +42,7 @@ impl Build { match platform { Platform::Web => { - crate::builder::build(&crate_config, false)?; + crate::builder::build(&crate_config)?; } Platform::Desktop => { crate::builder::build_desktop(&crate_config, false)?; diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 78eddb574..a780598e0 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -73,7 +73,7 @@ pub async fn serve_default( config: CrateConfig, start_browser: bool, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; + let first_build_result = crate::builder::build(&config)?; log::info!("🚀 Starting development server..."); @@ -134,7 +134,7 @@ pub async fn serve_hot_reload( config: CrateConfig, start_browser: bool, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; + let first_build_result = crate::builder::build(&config)?; log::info!("🚀 Starting development server..."); @@ -474,7 +474,7 @@ async fn ws_handler( } fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(config, true)?; + let result = builder::build(config)?; // change the websocket reload state to true; // the page will auto-reload. if config From bc94f36934a4444f4b19f283dfeae7ca4b171d82 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 3 Sep 2023 13:36:56 -0500 Subject: [PATCH 80/85] restrict tauri bundler version to 1.3.0 only --- packages/cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index b2beb2781..29bc880f3 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -78,7 +78,7 @@ toml_edit = "0.19.11" # dioxus-rsx = "0.0.1" # bundling -tauri-bundler = { version = "=1.3.*", features = ["native-tls-vendored"] } +tauri-bundler = { version = "1.3.0", features = ["native-tls-vendored"] } tauri-utils = "=1.4.*" dioxus-autofmt = { workspace = true } From 95887e836c1a3a99f5f1ac4675c2a72e40ef75a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Esp=C3=ADn?= Date: Mon, 4 Sep 2023 14:28:38 +0200 Subject: [PATCH 81/85] fix: Add the --locked param to every cli installation command (#1427) --- docs/guide/src/en/getting_started/web.md | 2 +- examples/PWA-example/README.md | 11 ++++++----- packages/cli/README.md | 4 +++- packages/cli/docs/src/installation.md | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index 48818bf8f..07139cce6 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -25,7 +25,7 @@ The Web is the best-supported target platform for Dioxus. To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [dioxus-cli](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli) which includes a build system, Wasm optimization, a dev server, and support hot reloading: ```shell -cargo install dioxus-cli +cargo install dioxus-cli --locked ``` Make sure the `wasm32-unknown-unknown` target for rust is installed: diff --git a/examples/PWA-example/README.md b/examples/PWA-example/README.md index d42a61311..d501df212 100644 --- a/examples/PWA-example/README.md +++ b/examples/PWA-example/README.md @@ -7,12 +7,13 @@ It is also very much usable as a template for your projects, if you're aiming to ## Try the example -Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`). +Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli --locked`). You can run `dx serve` in this directory to start the web server locally, or run `dx build --release` to build the project so you can deploy it on a separate web-server. ## Project Structure + ``` ├── Cargo.toml ├── Dioxus.toml @@ -33,12 +34,12 @@ You can run `dx serve` in this directory to start the web server locally, or run If you're just getting started with PWAs, here are some useful resources: -* [PWABuilder docs](https://docs.pwabuilder.com/#/) -* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) +- [PWABuilder docs](https://docs.pwabuilder.com/#/) +- [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) For service worker scripting (in JavaScript): -* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro) -* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers) +- [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro) +- [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers) If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though. diff --git a/packages/cli/README.md b/packages/cli/README.md index fcd9d25b4..d868b1c9d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11,7 +11,7 @@ It handles all building, bundling, development and publishing to simplify develo ### Install the stable version (recommended) ``` -cargo install dioxus-cli +cargo install dioxus-cli --locked ``` ### Install the latest development build through git @@ -28,6 +28,7 @@ This will download the CLI from the master branch, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). ### Install from local folder + ``` cargo install --path . --debug ``` @@ -52,6 +53,7 @@ You can use the `Dioxus.toml` file for further configuration. Some fields are mandatory, but the CLI tool will tell you which ones are missing. You can create a `Dioxus.toml` with all fields already set using `dx config init project-name`, or you can use this bare-bones template (only mandatory fields) to get started: + ```toml [application] name = "project-name" diff --git a/packages/cli/docs/src/installation.md b/packages/cli/docs/src/installation.md index 5030170f4..22803be61 100644 --- a/packages/cli/docs/src/installation.md +++ b/packages/cli/docs/src/installation.md @@ -16,8 +16,8 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). The published version of the Dioxus CLI is updated less often, but is more stable than the git version. ``` -cargo install dioxus-cli +cargo install dioxus-cli --locked ``` Run `dx --help` for a list of all the available commands. -Furthermore, you can run `dx --help` to get help with a specific command. \ No newline at end of file +Furthermore, you can run `dx --help` to get help with a specific command. From d5d974d5a3714e561b215fc19c1acbe2994ee325 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Mon, 4 Sep 2023 15:10:27 -0500 Subject: [PATCH 82/85] Bump the dioxus-cli to 0.4.1 (#1424) * bump the dioxus-cli version * bump DIOXUS_CLI_VERSION constant * Merge branch 'master' into bump-cli --- packages/cli/Cargo.toml | 4 ++-- packages/cli/src/builder.rs | 3 ++- packages/cli/src/cli/build.rs | 2 +- packages/cli/src/lib.rs | 2 +- packages/cli/src/server/web/mod.rs | 6 +++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index dcb4b8f68..f325430aa 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-cli" -version = { workspace = true } +version = "0.4.1" authors = ["Jonathan Kelley"] edition = "2021" description = "CLI tool for developing, testing, and publishing Dioxus apps" @@ -77,7 +77,7 @@ cargo-generate = "0.18" toml_edit = "0.19.11" # bundling -tauri-bundler = { version = "1.3.0", features = ["native-tls-vendored"] } +tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } tauri-utils = "=1.4.*" dioxus-autofmt = { workspace = true } diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 7f1b49ace..c4d1bde6f 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -22,7 +22,8 @@ pub struct BuildResult { pub elapsed_time: u128, } -pub fn build(config: &CrateConfig) -> Result { +#[allow(unused)] +pub fn build(config: &CrateConfig, quiet: bool) -> Result { // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [2] Generate the appropriate build folders // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index d701d2ee8..7fe29f8ce 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -42,7 +42,7 @@ impl Build { match platform { Platform::Web => { - crate::builder::build(&crate_config)?; + crate::builder::build(&crate_config, true)?; } Platform::Desktop => { crate::builder::build_desktop(&crate_config, false)?; diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index 2d5f6dfcb..c067ab148 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -1,4 +1,4 @@ -pub const DIOXUS_CLI_VERSION: &str = "0.1.5"; +pub const DIOXUS_CLI_VERSION: &str = "0.4.1"; pub mod builder; pub mod server; diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index a780598e0..bb1c00c99 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -73,7 +73,7 @@ pub async fn serve_default( config: CrateConfig, start_browser: bool, ) -> Result<()> { - let first_build_result = crate::builder::build(&config)?; + let first_build_result = crate::builder::build(&config, true)?; log::info!("🚀 Starting development server..."); @@ -134,7 +134,7 @@ pub async fn serve_hot_reload( config: CrateConfig, start_browser: bool, ) -> Result<()> { - let first_build_result = crate::builder::build(&config)?; + let first_build_result = crate::builder::build(&config, true)?; log::info!("🚀 Starting development server..."); @@ -474,7 +474,7 @@ async fn ws_handler( } fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(config)?; + let result = builder::build(config, true)?; // change the websocket reload state to true; // the page will auto-reload. if config From 6662892c10f97ca5fc50c5c40fd3b95dc9c93e47 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Tue, 5 Sep 2023 13:43:42 -0500 Subject: [PATCH 83/85] find attributes and components to drop in lazy nodes (#1376) --- packages/core/src/nodes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 2399c16b8..c83b1059a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -691,7 +691,7 @@ impl<'a> IntoDynNode<'a> for &Element<'a> { impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(cx.bump().alloc([self.call(cx)])) + DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()])) } } @@ -750,7 +750,7 @@ impl<'a> IntoTemplate<'a> for Element<'a> { } impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { - self.call(cx) + cx.render(self).unwrap() } } From e8ea228759ed8d716620af7583fb32a404bd151b Mon Sep 17 00:00:00 2001 From: Valentin Golev Date: Wed, 6 Sep 2023 19:49:29 +0200 Subject: [PATCH 84/85] add initial_value to textarea (#1434) --- packages/html/src/elements.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 7e5bed124..7800369a9 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1258,6 +1258,8 @@ builder_constructors! { spellcheck: BoolOrDefault DEFAULT, wrap: Wrap DEFAULT, value: String volatile, + + initial_value: String DEFAULT, }; From c8127e164b7f2a64f288dc97271cf5a5ed11da3e Mon Sep 17 00:00:00 2001 From: ealmloff Date: Wed, 6 Sep 2023 12:56:43 -0500 Subject: [PATCH 85/85] Url decode routes (#1407) * fix a few new clippy lints * url decode routes * fix catch all segments * fix clippy --- packages/router-macro/src/lib.rs | 3 +- packages/router-macro/src/query.rs | 2 +- packages/router-macro/src/route_tree.rs | 9 ++++-- packages/router-macro/src/segment.rs | 13 ++++++--- packages/router/Cargo.toml | 2 +- packages/router/src/lib.rs | 5 ++++ packages/router/src/routable.rs | 39 +++++++++++++++++++++++-- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index c949cf2f4..1078e5b9a 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -482,7 +482,8 @@ impl RouteEnum { let route = s; let (route, _hash) = route.split_once('#').unwrap_or((route, "")); let (route, query) = route.split_once('?').unwrap_or((route, "")); - let mut segments = route.split('/'); + let query = dioxus_router::exports::urlencoding::decode(query).unwrap_or(query.into()); + let mut segments = route.split('/').map(|s| dioxus_router::exports::urlencoding::decode(s).unwrap_or(s.into())); // skip the first empty segment if s.starts_with('/') { let _ = segments.next(); diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index 8b9db9e51..c97d3be29 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -14,7 +14,7 @@ impl QuerySegment { let ident = &self.ident; let ty = &self.ty; quote! { - let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query); + let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(&*query); } } diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 399d20e5e..6b89c80ab 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -312,7 +312,8 @@ impl<'a> RouteTreeSegmentData<'a> { quote! { { let mut segments = segments.clone(); - if let Some(segment) = segments.next() { + let segment = segments.next(); + if let Some(segment) = segment.as_deref() { if #segment == segment { #(#children)* } @@ -369,7 +370,7 @@ impl<'a> RouteTreeSegmentData<'a> { quote! { let mut trailing = String::from("/"); for seg in segments.clone() { - trailing += seg; + trailing += &*seg; trailing += "/"; } trailing.pop(); @@ -506,7 +507,9 @@ fn return_constructed( let remaining_segments = segments.clone(); let mut segments_clone = segments.clone(); let next_segment = segments_clone.next(); + let next_segment = next_segment.as_deref(); let segment_after_next = segments_clone.next(); + let segment_after_next = segment_after_next.as_deref(); match (next_segment, segment_after_next) { // This is the last segment, return the parsed route (None, _) | (Some(""), None) => { @@ -516,7 +519,7 @@ fn return_constructed( _ => { let mut trailing = String::new(); for seg in remaining_segments { - trailing += seg; + trailing += &*seg; trailing += "/"; } trailing.pop(); diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 62dea59a7..e13141dc0 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -59,6 +59,7 @@ impl RouteSegment { { let mut segments = segments.clone(); let segment = segments.next(); + let segment = segment.as_deref(); let parsed = if let Some(#segment) = segment { Ok(()) } else { @@ -80,7 +81,8 @@ impl RouteSegment { quote! { { let mut segments = segments.clone(); - let parsed = if let Some(segment) = segments.next() { + let segment = segments.next(); + let parsed = if let Some(segment) = segment.as_deref() { <#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) } else { Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name)) @@ -100,9 +102,12 @@ impl RouteSegment { quote! { { let parsed = { - let mut segments = segments.clone(); - let segments: Vec<_> = segments.collect(); - <#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) + let remaining_segments: Vec<_> = segments.collect(); + let mut new_segments: Vec<&str> = Vec::new(); + for segment in &remaining_segments { + new_segments.push(&*segment); + } + <#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&new_segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) }; match parsed { Ok(#name) => { diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 46d7536b6..f8f967ef2 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -17,7 +17,7 @@ gloo = { version = "0.8.0", optional = true } log = { workspace = true } thiserror = { workspace = true } futures-util = { workspace = true } -serde_urlencoded = { version = "0.7.1", optional = true } +urlencoding = "2.1.3" serde = { version = "1", features = ["derive"], optional = true } url = "2.3.1" wasm-bindgen = { workspace = true, optional = true } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 49b7ffc42..a92394513 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -82,3 +82,8 @@ pub mod prelude { mod utils { pub(crate) mod use_router_internal; } + +#[doc(hidden)] +pub mod exports { + pub use urlencoding; +} diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index d9f5b2346..39a5245a9 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -34,7 +34,7 @@ pub trait FromQuery { impl From<&'a str>> FromQuery for T { fn from_query(query: &str) -> Self { - T::from(query) + T::from(&*urlencoding::decode(query).expect("Failed to decode url encoding")) } } @@ -54,10 +54,22 @@ where type Err = ::Err; fn from_route_segment(route: &str) -> Result { - T::from_str(route) + match urlencoding::decode(route) { + Ok(segment) => T::from_str(&segment), + Err(err) => { + log::error!("Failed to decode url encoding: {}", err); + T::from_str(route) + } + } } } +#[test] +fn full_circle() { + let route = "testing 1234 hello world"; + assert_eq!(String::from_route_segment(route).unwrap(), route); +} + /// Something that can be converted to route segments pub trait ToRouteSegments { /// Display the route segments @@ -71,12 +83,33 @@ where fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for segment in self { write!(f, "/")?; - write!(f, "{}", segment)?; + let segment = segment.to_string(); + match urlencoding::decode(&segment) { + Ok(segment) => write!(f, "{}", segment)?, + Err(err) => { + log::error!("Failed to decode url encoding: {}", err); + write!(f, "{}", segment)? + } + } } Ok(()) } } +#[test] +fn to_route_segments() { + struct DisplaysRoute; + + impl std::fmt::Display for DisplaysRoute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let segments = vec!["hello", "world"]; + segments.display_route_segements(f) + } + } + + assert_eq!(DisplaysRoute.to_string(), "/hello/world"); +} + /// Something that can be created from route segments pub trait FromRouteSegments: Sized { /// The error that can occur when parsing route segments