From 8cfc437bfe4110d7f984428f01df90bdf8f8d9ec Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 9 Jul 2021 11:54:07 -0400 Subject: [PATCH] wip: some project refactor --- Cargo.toml | 3 +- packages/core/examples/async.rs | 7 +- packages/core/examples/borrowed.rs | 2 +- packages/core/src/arena.rs | 26 +- packages/core/src/bumpframe.rs | 2 +- packages/core/src/component.rs | 1 - packages/core/src/context.rs | 16 +- packages/core/src/diff.rs | 17 +- packages/core/src/events.rs | 2 +- packages/core/src/hooks.rs | 413 --------------------------- packages/core/src/lib.rs | 5 - packages/core/src/nodebuilder.rs | 10 +- packages/core/src/nodes.rs | 7 +- packages/core/src/scope.rs | 68 ++--- packages/core/src/serialize.rs | 9 + packages/core/src/signals.rs | 9 +- packages/core/src/util.rs | 40 ++- packages/core/src/virtual_dom.rs | 40 +-- packages/hooks/README.md | 61 ++++ packages/hooks/examples/lifecycle.rs | 5 +- packages/hooks/examples/reducer.rs | 1 + packages/hooks/src/lib.rs | 117 +------- packages/hooks/src/usecollection.rs | 76 +++++ packages/hooks/src/useref.rs | 0 packages/hooks/src/usesignal.rs | 0 packages/hooks/src/usestate.rs | 170 +++++++++++ packages/hooks/src/usetask.rs | 0 27 files changed, 430 insertions(+), 677 deletions(-) delete mode 100644 packages/core/src/hooks.rs create mode 100644 packages/hooks/README.md create mode 100644 packages/hooks/examples/reducer.rs create mode 100644 packages/hooks/src/usecollection.rs create mode 100644 packages/hooks/src/useref.rs create mode 100644 packages/hooks/src/usesignal.rs create mode 100644 packages/hooks/src/usestate.rs create mode 100644 packages/hooks/src/usetask.rs diff --git a/Cargo.toml b/Cargo.toml index c6cb15fba..89a6a7143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ dioxus-core-macro = { path="./packages/core-macro", version="0.1.0" } dioxus-html = { path="./packages/html", optional=true } dioxus-web = { path="./packages/web", optional=true } dioxus-webview = { path="./packages/webview", optional=true } -# dioxus-hooks = { path="./packages/hooks", version="0.0.0" } +dioxus-hooks = { path="./packages/hooks", version="0.0.0" } [features] @@ -49,6 +49,7 @@ members = [ "packages/html", "packages/web", "packages/webview", + "packages/hooks", # "packages/atoms", # "packages/ssr", # "packages/docsite", diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index a72a1366c..ddd67fd3d 100644 --- a/packages/core/examples/async.rs +++ b/packages/core/examples/async.rs @@ -6,16 +6,17 @@ use futures::Future; fn main() {} const App: FC<()> = |cx| { + // create a new future let mut fut = cx.use_hook( || { // - Box::pin(async { loop {} }) as Pin>> + async { loop {} } + // Box::pin(async { loop {} }) as Pin>> }, |f| f, |_| {}, ); - - cx.submit_task(fut); + // let g = unsafe { Pin::new_unchecked(fut) }; cx.submit_task(fut); diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 7f73234e5..1bd1c2ab5 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -20,7 +20,7 @@ struct ListItem { } fn app(cx: Context) -> VNode { - let (val, set_val) = use_state_classic(cx, || 0); + // let (val, set_val) = use_state_classic(cx, || 0); cx.render(LazyNodes::new(move |_nodecx| { todo!() diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 977abecfe..c8e519413 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -2,37 +2,33 @@ use std::{ cell::{RefCell, UnsafeCell}, collections::HashMap, rc::Rc, + sync::Arc, }; use crate::innerlude::*; use slotmap::{DefaultKey, SlotMap}; #[derive(Clone)] -pub struct ScopeArena(pub Rc>); -pub type ScopeMap = SlotMap; - -pub struct ScopeArenaInner { - pub(crate) arena: UnsafeCell, - locks: HashMap, +pub struct SharedArena { + pub components: Arc>, } +pub type ScopeMap = SlotMap; enum MutStatus { Immut, Mut, } -impl ScopeArena { +impl SharedArena { pub fn new(arena: ScopeMap) -> Self { - ScopeArena(Rc::new(RefCell::new(ScopeArenaInner { - arena: UnsafeCell::new(arena), - locks: Default::default(), - }))) + let components = Arc::new(UnsafeCell::new(arena)); + SharedArena { components } } /// THIS METHOD IS CURRENTLY UNSAFE /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS pub fn try_get(&self, idx: ScopeIdx) -> Result<&Scope> { - let inner = unsafe { &*self.0.borrow().arena.get() }; + let inner = unsafe { &*self.components.get() }; let scope = inner.get(idx); scope.ok_or_else(|| Error::FatalInternal("Scope not found")) } @@ -40,7 +36,7 @@ impl ScopeArena { /// THIS METHOD IS CURRENTLY UNSAFE /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS pub fn try_get_mut(&self, idx: ScopeIdx) -> Result<&mut Scope> { - let inner = unsafe { &mut *self.0.borrow().arena.get() }; + let inner = unsafe { &mut *self.components.get() }; let scope = inner.get_mut(idx); scope.ok_or_else(|| Error::FatalInternal("Scope not found")) } @@ -56,7 +52,7 @@ impl ScopeArena { /// THIS METHOD IS CURRENTLY UNSAFE /// THERE ARE NO CHECKS TO VERIFY THAT WE ARE ALLOWED TO DO THIS pub fn with(&self, f: impl FnOnce(&mut ScopeMap) -> T) -> Result { - let inner = unsafe { &mut *self.0.borrow().arena.get() }; + let inner = unsafe { &mut *self.components.get() }; Ok(f(inner)) // todo!() } @@ -80,7 +76,7 @@ impl ScopeArena { } pub fn try_remove(&self, id: ScopeIdx) -> Result { - let inner = unsafe { &mut *self.0.borrow().arena.get() }; + let inner = unsafe { &mut *self.components.get() }; inner .remove(id) .ok_or_else(|| Error::FatalInternal("Scope not found")) diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs index 73face15c..bc0ca97d8 100644 --- a/packages/core/src/bumpframe.rs +++ b/packages/core/src/bumpframe.rs @@ -1,5 +1,5 @@ use crate::hooklist::HookList; -use crate::{arena::ScopeArena, innerlude::*}; +use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; use futures::FutureExt; diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index f96cecc3e..6687bb873 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -43,7 +43,6 @@ pub fn fc_to_builder(_: FC) -> T::Builder { } /// Create inline fragments -/// ------------------------ /// /// Fragments capture a series of children without rendering extra nodes. /// diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 4e6d0ddff..848212941 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,5 +1,5 @@ use crate::hooklist::HookList; -use crate::{arena::ScopeArena, innerlude::*}; +use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; use futures::FutureExt; @@ -40,10 +40,11 @@ use std::{ pub struct Context<'src, T> { pub props: &'src T, pub scope: &'src Scope, - pub tasks: &'src AppendList<&'src mut DTask>, + pub tasks: &'src AppendList<&'src mut DTask<'src>>, } -pub type DTask = Pin>>; +pub type DTask<'s> = dyn Future + 's; +// pub type DTask = Pin>>; impl<'src, T> Copy for Context<'src, T> {} impl<'src, T> Clone for Context<'src, T> { @@ -293,12 +294,15 @@ Any function prefixed with "use" should not be called conditionally. /// /// Tasks can't return anything, but they can be controlled with the returned handle /// - /// Tasks will only run until the component renders again. If you want your task to last longer than one frame, you'll need - /// to store it somehow. + /// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it + /// is considered "stable" + /// + /// /// pub fn submit_task( &self, - mut task: &'src mut Pin + 'static>>, + mut task: &'src mut DTask<'src>, + // mut task: &'src mut Pin + 'static>>, ) -> TaskHandle { self.tasks.push(task); // let mut g = self.task.borrow_mut(); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 106e0a27b..f70ef0b8e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -22,10 +22,7 @@ //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{ - arena::{ScopeArena, ScopeArenaInner}, - innerlude::*, -}; +use crate::{arena::SharedArena, innerlude::*}; use fxhash::{FxHashMap, FxHashSet}; use std::{ @@ -96,7 +93,7 @@ pub trait RealDom<'a> { /// subscriptions and props changes. pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { pub dom: &'real mut Dom, - pub components: &'bump ScopeArena, + pub components: &'bump SharedArena, pub cur_idx: ScopeIdx, pub diffed: FxHashSet, pub event_queue: EventQueue, @@ -106,7 +103,7 @@ pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { pub fn new( dom: &'real mut Dom, - components: &'bump ScopeArena, + components: &'bump SharedArena, cur_idx: ScopeIdx, event_queue: EventQueue, ) -> Self { @@ -376,7 +373,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // self.dom.save_known_root(root_id); log::debug!("Mounting a new component"); - let caller: Weak = Rc::downgrade(&component.caller); + let caller: Weak = Rc::downgrade(&component.caller); // We're modifying the component arena while holding onto references into the assoiated bump arenas of its children // those references are stable, even if the component arena moves around in memory, thanks to the bump arenas. @@ -413,7 +410,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // yaaaaay lifetimes out of thin air // really tho, we're merging the frame lifetimes together - let inner: &'bump mut _ = unsafe { &mut *self.components.0.borrow().arena.get() }; + let inner: &'bump mut _ = unsafe { &mut *self.components.components.get() }; let new_component = inner.get_mut(idx).unwrap(); // Actually initialize the caller's slot with the right address @@ -1233,7 +1230,7 @@ enum KeyedPrefixResult { /// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like /// "InsertBefore". struct RealChildIterator<'a> { - scopes: &'a ScopeArena, + scopes: &'a SharedArena, // Heuristcally we should never bleed into 5 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane @@ -1241,7 +1238,7 @@ struct RealChildIterator<'a> { } impl<'a> RealChildIterator<'a> { - fn new(starter: &'a VNode<'a>, scopes: &'a ScopeArena) -> Self { + fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self { Self { scopes, stack: smallvec::smallvec![(0, starter)], diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3a34117cf..e6bb1a911 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -6,7 +6,7 @@ use std::{ops::Deref, rc::Rc}; -use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode}; +use crate::innerlude::{RealDomNode, ScopeIdx}; #[derive(Debug)] pub struct EventTrigger { diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs deleted file mode 100644 index fa376bd6e..000000000 --- a/packages/core/src/hooks.rs +++ /dev/null @@ -1,413 +0,0 @@ -//! Useful, foundational hooks that 3rd parties can implement. -//! Currently implemented: -//! - [x] use_ref -//! - [x] use_state -//! - [ ] use_reducer -//! - [ ] use_effect - -use crate::innerlude::Context; - -use std::{ - cell::{Cell, RefCell}, - ops::{Deref, DerefMut}, - rc::Rc, -}; - -/// Store state between component renders! -/// When called, this hook retrives a stored value and provides a setter to update that value. -/// When the setter is called, the component is re-ran with the new value. -/// -/// This is behaves almost exactly the same way as React's "use_state". -/// -/// Usage: -/// ```ignore -/// static Example: FC<()> = |cx| { -/// let (counter, set_counter) = use_state(cx, || 0); -/// let increment = |_| set_couter(counter + 1); -/// let decrement = |_| set_couter(counter + 1); -/// -/// html! { -///
-///

"Counter: {counter}"

-/// -/// -///
-/// } -/// } -/// ``` -pub fn use_state_classic<'a, 'c, T: 'static, F: FnOnce() -> T, P>( - cx: Context<'a, P>, - initial_state_fn: F, -) -> (&'a T, &'a Rc) { - struct UseState { - new_val: Rc>>, - current_val: T, - caller: Rc, - } - - cx.use_hook( - move || UseState { - new_val: Rc::new(RefCell::new(None)), - current_val: initial_state_fn(), - caller: Rc::new(|_| println!("setter called!")), - }, - move |hook| { - log::debug!("Use_state set called"); - let inner = hook.new_val.clone(); - let scheduled_update = cx.schedule_update(); - - // get ownership of the new val and replace the current with the new - // -> as_ref -> borrow_mut -> deref_mut -> take - // -> rc -> &RefCell -> RefMut -> &Option -> T - if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { - hook.current_val = new_val; - } - - // todo: swap out the caller with a subscription call and an internal update - hook.caller = Rc::new(move |new_val| { - // update the setter with the new value - let mut new_inner = inner.as_ref().borrow_mut(); - *new_inner = Some(new_val); - - // Ensure the component gets updated - scheduled_update(); - }); - - // box gets derefed into a ref which is then taken as ref with the hook - (&hook.current_val, &hook.caller) - }, - |_| {}, - ) -} - -use crate::innerlude::*; -use std::{ - fmt::{Debug, Display}, - pin::Pin, -}; - -pub struct UseState<'a, T: 'static> { - inner: &'a UseStateInner, -} -impl Copy for UseState<'_, T> {} -impl<'a, T> Clone for UseState<'a, T> -where - T: 'static, -{ - fn clone(&self) -> Self { - UseState { inner: self.inner } - } -} - -impl<'a, T: 'static> UseState<'a, T> { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.inner.update_scheuled.get() { - self.inner.update_scheuled.set(true); - (self.inner.callback)(); - } - } - - pub fn set(&self, new_val: T) { - self.needs_update(); - *self.inner.wip.borrow_mut() = Some(new_val); - } - - pub fn get(&self) -> &T { - &self.inner.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.inner.wip.borrow() - } -} -impl<'a, T: 'static + ToOwned> UseState<'a, T> { - pub fn get_mut(self) -> RefMut<'a, T> { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.inner.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.inner.current_val.to_owned()); - } - slot.as_mut().unwrap() - }) - } -} - -impl<'a, T: 'static> std::ops::Deref for UseState<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner.current_val - } -} - -use std::ops::{Add, AddAssign, Sub, SubAssign}; -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.inner.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.add(rhs)); - } -} -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.inner.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.sub(rhs)); - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.current_val) - } -} -struct UseStateInner { - current_val: T, - update_scheuled: Cell, - callback: Rc, - wip: RefCell>, -} - -/// Store state between component renders! -/// -/// ## The "King" of state hooks -/// -/// The Dioxus version of `useState` is the "king daddy" of state management. It allows you to ergonomically store and -/// modify state between component renders. When the state is updated, the component will re-render. -/// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// -/// Usage: -/// ```ignore -/// const Example: FC<()> = |cx| { -/// let counter = use_state(cx, || 0); -/// let increment = |_| counter += 1; -/// let decrement = |_| counter += 1; -/// -/// html! { -///
-///

"Counter: {counter}"

-/// -/// -///
-/// } -/// } -/// ``` -pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>( - cx: Context<'a, P>, - initial_state_fn: F, -) -> UseState { - cx.use_hook( - move || UseStateInner { - current_val: initial_state_fn(), - callback: cx.schedule_update(), - wip: RefCell::new(None), - update_scheuled: Cell::new(false), - }, - move |hook| { - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - if new_val.is_some() { - hook.current_val = new_val.take().unwrap(); - } - - UseState { inner: &*hook } - }, - |_| {}, - ) -} - -/// Store a mutable value between renders! -/// To read the value, borrow the ref. -/// To change it, use modify. -/// Modifications to this value do not cause updates to the component -/// Attach to inner context reference, so context can be consumed -pub fn use_ref<'a, T: 'static, P>( - cx: Context<'a, P>, - initial_state_fn: impl FnOnce() -> T + 'static, -) -> &'a RefCell { - cx.use_hook(|| RefCell::new(initial_state_fn()), |state| &*state, |_| {}) -} - -struct UseReducer { - new_val: Rc>>, - current_val: T, - caller: Box, -} - -/// Store state between component renders! -/// When called, this hook retrives a stored value and provides a setter to update that value. -/// When the setter is called, the component is re-ran with the new value. -/// -/// This is behaves almost exactly the same way as React's "use_state". -/// -pub fn use_reducer<'a, 'c, State: 'static, Action: 'static, P>( - cx: Context<'a, P>, - initial_state_fn: impl FnOnce() -> State, - _reducer: impl Fn(&mut State, Action), -) -> (&'a State, &'a Box) { - cx.use_hook( - move || UseReducer { - new_val: Rc::new(RefCell::new(None)), - current_val: initial_state_fn(), - caller: Box::new(|_| println!("setter called!")), - }, - move |hook| { - let _inner = hook.new_val.clone(); - let scheduled_update = cx.schedule_update(); - - // get ownership of the new val and replace the current with the new - // -> as_ref -> borrow_mut -> deref_mut -> take - // -> rc -> &RefCell -> RefMut -> &Option -> T - if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { - hook.current_val = new_val; - } - - // todo: swap out the caller with a subscription call and an internal update - hook.caller = Box::new(move |_new_val| { - // update the setter with the new value - // let mut new_inner = inner.as_ref().borrow_mut(); - // *new_inner = Some(new_val); - - // Ensure the component gets updated - scheduled_update(); - }); - - // box gets derefed into a ref which is then taken as ref with the hook - (&hook.current_val, &hook.caller) - }, - |_| {}, - ) -} - -/// Use model makes it easy to use "models" as state for components. To modify the model, call "modify" and a clone of the -/// current model will be made, with a RefMut lock on it. Dioxus will never run your components multithreaded, so you can -/// be relatively sure that this won't fail in practice -pub fn use_model<'a, T: ToOwned + 'static, P>( - cx: Context<'a, P>, - f: impl FnOnce() -> T, -) -> &'a UseModel { - cx.use_hook( - move || { - let real = f(); - let wip = RefCell::new(real.to_owned()); - let update = cx.schedule_update(); - UseModel { real, wip, update } - }, - |hook| { - hook.real = hook.wip.borrow().to_owned(); - &*hook - }, - |_| {}, - ) -} - -pub struct UseModel { - real: T, - wip: RefCell, - update: Rc, -} - -use std::cell::{Ref, RefMut}; -impl Deref for UseModel { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.real - } -} - -impl Display for UseModel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.real) - } -} - -impl UseModel { - pub fn get_mut(&self) -> RefMut<'_, T> { - (self.update)(); - self.wip.borrow_mut() - } - pub fn modify(&self, f: impl FnOnce(&mut T)) { - (self.update)(); - let mut g = self.get_mut(); - let r = g.deref_mut(); - f(r) - } -} - -// #[cfg(test)] -mod tests { - - use crate::prelude::*; - - enum Actions { - Incr, - Decr, - } - - // #[allow(unused)] - // static Example: FC<()> = |cx| { - // let (count, reduce) = use_reducer( - // &cx, - // || 0, - // |count, action| match action { - // Actions::Incr => *count += 1, - // Actions::Decr => *count -= 1, - // }, - // ); - - // cx.render(rsx! { - // div { - // h1 {"Count: {count}"} - // button { - // "Increment" - // onclick: move |_| reduce(Actions::Incr) - // } - // button { - // "Decrement" - // onclick: move |_| reduce(Actions::Decr) - // } - // } - // }) - // }; -} - -pub fn use_is_initialized<'a, P>(cx: Context<'a, P>) -> bool { - let val = use_ref(cx, || false); - match *val.borrow() { - true => true, - false => { - // - *val.borrow_mut() = true; - false - } - } -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 19972f68f..b23b98175 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -19,7 +19,6 @@ pub mod diff; pub mod error; pub mod events; pub mod hooklist; -pub mod hooks; pub mod nodebuilder; pub mod nodes; pub mod scope; @@ -41,7 +40,6 @@ pub(crate) mod innerlude { pub use crate::diff::*; pub use crate::error::*; pub use crate::events::*; - pub use crate::hooks::*; pub use crate::nodebuilder::*; pub use crate::nodes::*; pub use crate::scope::*; @@ -84,7 +82,4 @@ pub mod prelude { pub use crate::diff::DiffMachine; pub use crate::virtual_dom::ScopeIdx; - - // pub use crate::debug_renderer::DebugRenderer; - pub use crate::hooks::*; } diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index d225523d7..0255682a1 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -14,10 +14,9 @@ use bumpalo::Bump; use crate::{ events::VirtualEvent, - innerlude::{Properties, Scope, VComponent, VText, FC}, + innerlude::{Properties, RealDomNode, Scope, VComponent, VText, FC}, nodes::{Attribute, Listener, NodeKey, VNode}, prelude::{VElement, VFragment}, - virtual_dom::RealDomNode, }; /// A virtual DOM element builder. @@ -509,6 +508,7 @@ where /// .finish(); /// ``` pub fn iter_child(mut self, nodes: impl IntoIterator>) -> Self { + todo!(); let len_before = self.children.len(); for item in nodes { todo!() @@ -542,13 +542,13 @@ impl<'a> IntoIterator for VNode<'a> { } } impl<'a> IntoVNode<'a> for VNode<'a> { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, _: NodeFactory<'a>) -> VNode<'a> { self } } impl<'a> IntoVNode<'a> for &VNode<'a> { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { + fn into_vnode(self, _: NodeFactory<'a>) -> VNode<'a> { self.clone() } } @@ -801,7 +801,7 @@ impl<'a> NodeFactory<'a> { use std::fmt::Debug; impl Debug for NodeFactory<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e116a32b6..c97096624 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,11 +4,10 @@ //! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick. use crate::{ - arena::ScopeArena, + arena::SharedArena, events::VirtualEvent, - innerlude::{Context, Properties, Scope, ScopeIdx, FC}, + innerlude::{Context, Properties, RealDom, RealDomNode, Scope, ScopeIdx, FC}, nodebuilder::{text3, NodeFactory}, - virtual_dom::RealDomNode, }; use bumpalo::Bump; use std::{ @@ -165,7 +164,7 @@ impl<'a> VNode<'a> { } } - pub fn get_mounted_id(&self, components: &ScopeArena) -> Option { + pub fn get_mounted_id(&self, components: &SharedArena) -> Option { match self { VNode::Element(el) => Some(el.dom_id.get()), VNode::Text(te) => Some(te.dom_id.get()), diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 291cb1f4f..c022ea09d 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,5 @@ use crate::hooklist::HookList; -use crate::{arena::ScopeArena, innerlude::*}; +use crate::{arena::SharedArena, innerlude::*}; use appendlist::AppendList; use bumpalo::Bump; use futures::FutureExt; @@ -17,6 +17,11 @@ use std::{ rc::{Rc, Weak}, }; +// We need to pin the hook so it doesn't move as we initialize the list of hooks +type Hook = Box; +type EventChannel = Rc; +pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> VNode<'b>; + /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -35,7 +40,7 @@ pub struct Scope { // A reference to the list of components. // This lets us traverse the component list whenever we need to access our parent or children. - pub arena_link: ScopeArena, + pub arena_link: SharedArena, pub shared_contexts: RefCell>>, @@ -46,7 +51,7 @@ pub struct Scope { pub event_channel: Rc, - pub caller: Weak, + pub caller: Weak, // ========================== // slightly unsafe stuff @@ -71,10 +76,6 @@ pub struct Scope { pub(crate) suspended_tasks: Vec<*mut Pin>>>>, } -// We need to pin the hook so it doesn't move as we initialize the list of hooks -type Hook = Box; -type EventChannel = Rc; - impl Scope { // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) // we are going to break this lifetime by force in order to save it on ourselves. @@ -84,12 +85,12 @@ impl Scope { // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom pub fn new<'creator_node>( - caller: Weak, + caller: Weak, arena_idx: ScopeIdx, parent: Option, height: u32, event_channel: EventChannel, - arena_link: ScopeArena, + arena_link: SharedArena, child_nodes: &'creator_node [VNode<'creator_node>], ) -> Self { log::debug!( @@ -98,25 +99,6 @@ impl Scope { arena_idx ); - // The function to run this scope is actually located in the parent's bump arena. - // Every time the parent is updated, that function is invalidated via double-buffering wiping the old frame. - // If children try to run this invalid caller, it *will* result in UB. - // - // During the lifecycle progression process, this caller will need to be updated. Right now, - // until formal safety abstractions are implemented, we will just use unsafe to "detach" the caller - // lifetime from the bump arena, exposing ourselves to this potential for invalidation. Truthfully, - // this is a bit of a hack, but will remain this way until we've figured out a cleaner solution. - // - // Not the best solution, so TODO on removing this in favor of a dedicated resource abstraction. - let caller = unsafe { - std::mem::transmute::< - Weak, - Weak, - // Weak>, - // Weak>, - >(caller) - }; - let child_nodes = unsafe { std::mem::transmute(child_nodes) }; Self { @@ -137,18 +119,8 @@ impl Scope { } } - pub fn update_caller<'creator_node>(&mut self, caller: Weak) { - // pub fn update_caller<'creator_node>(&mut self, caller: Weak>) { - let broken_caller = unsafe { - std::mem::transmute::< - Weak, - Weak, - // Weak>, - // Weak>, - >(caller) - }; - - self.caller = broken_caller; + pub fn update_caller<'creator_node>(&mut self, caller: Weak) { + self.caller = caller; } pub fn update_children<'creator_node>( @@ -159,10 +131,6 @@ impl Scope { self.child_nodes = child_nodes; } - /// Create a new context and run the component with references from the Virtual Dom - /// This function downcasts the function pointer based on the stored props_type - /// - /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) pub fn run_scope<'sel>(&'sel mut self) -> Result<()> { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. @@ -180,19 +148,17 @@ impl Scope { .ok_or(Error::FatalInternal("Failed to get caller"))?; // Cast the caller ptr from static to one with our own reference - let c2: &OpaqueComponent = caller.as_ref(); - let c3: &OpaqueComponent = unsafe { std::mem::transmute(c2) }; + let c3: &WrappedCaller = caller.as_ref(); - self.frames.cur_frame_mut().head_node = unsafe { self.own_vnodes(c3) }; + self.frames.cur_frame_mut().head_node = unsafe { self.call_user_component(c3) }; Ok(()) } // this is its own function so we can preciesly control how lifetimes flow - unsafe fn own_vnodes<'a>(&'a self, f: &OpaqueComponent) -> VNode<'static> { - let new_head: VNode<'a> = f(self); - let out: VNode<'static> = std::mem::transmute(new_head); - out + unsafe fn call_user_component<'a>(&'a self, caller: &WrappedCaller) -> VNode<'static> { + let new_head: VNode<'a> = caller(self); + std::mem::transmute(new_head) } // A safe wrapper around calling listeners diff --git a/packages/core/src/serialize.rs b/packages/core/src/serialize.rs index 681660823..81824a0d8 100644 --- a/packages/core/src/serialize.rs +++ b/packages/core/src/serialize.rs @@ -1,3 +1,12 @@ +//! Serialization +//! ------------- +//! +//! +//! +//! +//! +//! + use crate::prelude::ScopeIdx; use serde::{Deserialize, Serialize}; diff --git a/packages/core/src/signals.rs b/packages/core/src/signals.rs index de846de3b..9b1639c85 100644 --- a/packages/core/src/signals.rs +++ b/packages/core/src/signals.rs @@ -1 +1,8 @@ -//! Support for jumping out of the diff engine +//! Signas: Avoid the Diff Engine +//! ----------------------------- +//! +//! +//! +//! +//! +//! diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 8135dd1bc..6a6614e1e 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -1,13 +1,15 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::{RefCell, RefMut}, + rc::Rc, + vec::Drain, +}; use crate::innerlude::*; -// We actually allocate the properties for components in their parent's properties -// We then expose a handle to use those props for render in the form of "OpaqueComponent" -pub type OpaqueComponent = dyn for<'b> Fn(&'b Scope) -> VNode<'b>; - #[derive(PartialEq, Debug, Clone, Default)] -pub struct EventQueue(pub Rc>>); +pub struct EventQueue { + pub queue: Rc>>, +} impl EventQueue { pub fn new_channel(&self, height: u32, idx: ScopeIdx) -> Rc { @@ -15,9 +17,17 @@ impl EventQueue { let marker = HeightMarker { height, idx }; Rc::new(move || { log::debug!("channel updated {:#?}", marker); - inner.0.as_ref().borrow_mut().push(marker) + inner.queue.as_ref().borrow_mut().push(marker) }) } + + pub fn sort_unstable(&self) { + self.queue.borrow_mut().sort_unstable() + } + + pub fn borrow_mut(&self) -> RefMut> { + self.queue.borrow_mut() + } } /// A helper type that lets scopes be ordered by their height @@ -39,6 +49,22 @@ impl PartialOrd for HeightMarker { } } +/// The `RealDomNode` is an ID handle that corresponds to a foreign DOM node. +/// +/// "u64" was chosen for two reasons +/// - 0 cost hashing +/// - use with slotmap and other versioned slot arenas +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RealDomNode(pub u64); +impl RealDomNode { + pub fn new(id: u64) -> Self { + Self(id) + } + pub fn empty() -> Self { + Self(u64::MIN) + } +} + pub struct DebugDom { counter: u64, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 947e00729..0baea5655 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -20,7 +20,7 @@ //! Additional functionality is defined in the respective files. use crate::tasks::TaskQueue; -use crate::{arena::ScopeArena, innerlude::*}; +use crate::{arena::SharedArena, innerlude::*}; use slotmap::DefaultKey; use slotmap::SlotMap; use std::{any::TypeId, fmt::Debug, rc::Rc}; @@ -35,7 +35,7 @@ pub struct VirtualDom { /// /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. - pub components: ScopeArena, + pub components: SharedArena, /// The index of the root component /// Should always be the first (gen=0, id=0) @@ -48,7 +48,7 @@ pub struct VirtualDom { /// a strong allocation to the "caller" for the original component and its props #[doc(hidden)] - _root_caller: Rc, + _root_caller: Rc, /// Type of the original cx. This is stored as TypeId so VirtualDom does not need to be generic. /// @@ -58,22 +58,6 @@ pub struct VirtualDom { _root_prop_type: std::any::TypeId, } -/// The `RealDomNode` is an ID handle that corresponds to a foreign DOM node. -/// -/// "u64" was chosen for two reasons -/// - 0 cost hashing -/// - use with slotmap and other versioned slot arenas -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct RealDomNode(pub u64); -impl RealDomNode { - pub fn new(id: u64) -> Self { - Self(id) - } - pub fn empty() -> Self { - Self(u64::MIN) - } -} - // ====================================== // Public Methods for the VirtualDom // ====================================== @@ -142,11 +126,11 @@ impl VirtualDom { /// let dom = VirtualDom::new(Example); /// ``` pub fn new_with_props(root: FC

, root_props: P) -> Self { - let components = ScopeArena::new(SlotMap::new()); + let components = SharedArena::new(SlotMap::new()); // Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents // Here, we need to make it manually, using an RC to force the Weak reference to stick around for the main scope. - let _root_caller: Rc = Rc::new(move |scope| { + let _root_caller: Rc = Rc::new(move |scope| { // let _root_caller: Rc> = Rc::new(move |scope| { // the lifetime of this closure is just as long as the lifetime on the scope reference // this closure moves root props (which is static) into this closure @@ -283,12 +267,8 @@ impl VirtualDom { &'bump self, diff_machine: &'_ mut DiffMachine<'a, 'bump, Dom>, ) -> Result<()> { - // Add this component to the list of components that need to be difed - // #[allow(unused_assignments)] - // let mut cur_height: u32 = 0; - // Now, there are events in the queue - let mut updates = self.event_queue.0.as_ref().borrow_mut(); + let mut updates = self.event_queue.queue.as_ref().borrow_mut(); // Order the nodes by their height, we want the nodes with the smallest depth on top // This prevents us from running the same component multiple times @@ -299,6 +279,7 @@ impl VirtualDom { // Iterate through the triggered nodes (sorted by height) and begin to diff them for update in updates.drain(..) { log::debug!("Running updates for: {:#?}", update); + // Make sure this isn't a node we've already seen, we don't want to double-render anything // If we double-renderer something, this would cause memory safety issues if diff_machine.seen_nodes.contains(&update.idx) { @@ -310,20 +291,13 @@ impl VirtualDom { // Start a new mutable borrow to components // We are guaranteeed that this scope is unique because we are tracking which nodes have modified - let cur_component = self.components.try_get_mut(update.idx).unwrap(); - // let inner: &'s mut _ = unsafe { &mut *self.components.0.borrow().arena.get() }; - // let cur_component = inner.get_mut(update.idx).unwrap(); cur_component.run_scope()?; - // diff_machine.change_list.load_known_root(1); let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); - // let (old, new) = cur_component.get_frames_mut(); diff_machine.diff_node(old, new); - // cur_height = cur_component.height; - // log::debug!( // "Processing update: {:#?} with height {}", // &update.idx, diff --git a/packages/hooks/README.md b/packages/hooks/README.md new file mode 100644 index 000000000..d0602c3dd --- /dev/null +++ b/packages/hooks/README.md @@ -0,0 +1,61 @@ +# Common hooks for Dioxus + +This crate includes some basic useful hooks for dioxus: + +- use_state +- use_ref +- use_collection +- use_task +- use_signal + +## use_state + +The king daddy of state hooks. + +You can always use it "normally" with the `split` method: + +```rust +// Normal usage: +let value = use_state(cx, || 10); + +// "Classic" usage: +let (value, set_value) = use_state(cx, || 0).classic(); +``` + +## use_ref + + +## use_rwlock +A multithreaded form of RwLock for use in tasks +```rust +let val = use_rwlock(cx, || 10); +use_task((), || async loop { + *val.write().unwrap() += 1; + async_std::task::delay(Duration::from_ms(1000)).await; +}); +use_task((), || async loop { + *val.write().unwrap() -= 1; + async_std::task::delay(Duration::from_ms(500)).await; +}); +``` + +## use_hashmap +Store a memoized collection with similar semantics to use_state. Comes with a bunch of utility methods to make working with collections easier. Is essentially a wrapper over the immutable hashmap in im-rc. + +```rust +let todos = use_hashmap(cx, |map| map.insert("bob", "bill")); +cx.render(rsx!( + button { onclick: move |_| todos.insert("bob", "bill") + "add random todo" + } +) + +``` + +## use_task + +use_task submits a task to the dioxus task queue to be progressed during Dioxus's async event loop. The task must not return anything + + +## use_signal + diff --git a/packages/hooks/examples/lifecycle.rs b/packages/hooks/examples/lifecycle.rs index 12b8f6ff6..87c4e5559 100644 --- a/packages/hooks/examples/lifecycle.rs +++ b/packages/hooks/examples/lifecycle.rs @@ -1,7 +1,4 @@ -fn main() { - // let mut s = Context { props: &() }; - // let g = Component(&mut s); -} +fn main() {} // struct CompState { // tasks: Vec<()>, diff --git a/packages/hooks/examples/reducer.rs b/packages/hooks/examples/reducer.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/hooks/examples/reducer.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 631f7c47b..017395172 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -2,118 +2,5 @@ use std::collections::HashMap; use dioxus_core::prelude::*; -/* -a form of use_state explicitly for map-style collections (BTreeMap, HashMap, etc). - -Why? ---- -Traditionally, it's possible to use the "use_state" hook for collections in the React world. -Adding a new entry would look something similar to: - -```js -let (map, set_map) = useState({}); -set_map({ ...map, [key]: value }); -``` -The new value then causes the appropriate update when passed into children. - -This is moderately efficient because the fields of the map are moved, but the data itself is not cloned. -However, if you used similar approach with Dioxus: - -```rust -let (map, set_map) = use_state(cx, || HashMap::new()); -set_map({ - let mut newmap = map.clone(); - newmap.set(key, value); - newmap -}) -``` -Unfortunately, you'd be cloning the entire state every time a value is changed. The obvious solution is to -wrap every element in the HashMap with an Rc. That way, cloning the HashMap is on par with its JS equivalent. - -Fortunately, we can make this operation even more efficient in Dioxus, leveraging the borrow rules of Rust. - -This hook provides a memoized collection, memoized setters, and memoized getters. This particular hook is -extremely powerful for implementing lists and supporting core state management needs for small apps. - -If you need something even more powerful, check out the dedicated atomic state management Dioxus Dataflow, which -uses the same memoization on top of the use_context API. - -Here's a fully-functional todo app using the use_map API: -```rust -static TodoList: FC<()> = |cx| { - let todos = use_map(cx, || HashMap::new()); - let input = use_ref(|| None); - - cx.render(rsx!{ - div { - button { - "Add todo" - onclick: move |_| { - let new_todo = TodoItem::new(input.contents()); - todos.insert(new_todo.id.clone(), new_todo); - input.clear(); - } - } - button { - "Clear todos" - onclick: move |_| todos.clear() - } - input { - placeholder: "What needs to be done?" - ref: input - } - ul { - {todos.iter().map(|todo| rsx!( - li { - key: todo.id - span { "{todo.content}" } - button {"x", onclick: move |_| todos.remove(todo.key.clone())} - } - ))} - } - } - }) -} - -``` - -*/ -fn use_map() {} - -// a form of "use_state" that allows collection memoization -// Elements are received as Rc in case the underlying collection is shuffled around -// Setters/getters can be generated -fn use_collection<'a, T: Collection>( - cx: &impl Scoped<'a>, - f: impl Fn() -> T, -) -> CollectionHandle<'a, T> { - cx.use_hook( - || {}, - |h| { - // - CollectionHandle { - _p: Default::default(), - } - }, - |h| {}, - ) -} - -struct CollectionMemo {} - -struct CollectionHandle<'a, T: Collection> { - _p: std::marker::PhantomData<&'a T>, -} - -trait Collection {} -impl Collection for std::collections::HashMap {} - -struct MapCollection { - inner: HashMap, -} - -impl MapCollection { - fn set(&self, key: K, val: V) { - // - } -} +mod usestate; +pub use usestate::{use_state, UseState}; diff --git a/packages/hooks/src/usecollection.rs b/packages/hooks/src/usecollection.rs new file mode 100644 index 000000000..cb696a214 --- /dev/null +++ b/packages/hooks/src/usecollection.rs @@ -0,0 +1,76 @@ +/* +a form of use_state explicitly for map-style collections (BTreeMap, HashMap, etc). + +Why? +--- +Traditionally, it's possible to use the "use_state" hook for collections in the React world. +Adding a new entry would look something similar to: + +```js +let (map, set_map) = useState({}); +set_map({ ...map, [key]: value }); +``` +The new value then causes the appropriate update when passed into children. + +This is moderately efficient because the fields of the map are moved, but the data itself is not cloned. +However, if you used similar approach with Dioxus: + +```rust +let (map, set_map) = use_state(cx, || HashMap::new()); +set_map({ + let mut newmap = map.clone(); + newmap.set(key, value); + newmap +}) +``` +Unfortunately, you'd be cloning the entire state every time a value is changed. The obvious solution is to +wrap every element in the HashMap with an Rc. That way, cloning the HashMap is on par with its JS equivalent. + +Fortunately, we can make this operation even more efficient in Dioxus, leveraging the borrow rules of Rust. + +This hook provides a memoized collection, memoized setters, and memoized getters. This particular hook is +extremely powerful for implementing lists and supporting core state management needs for small apps. + +If you need something even more powerful, check out the dedicated atomic state management Dioxus Dataflow, which +uses the same memoization on top of the use_context API. + +Here's a fully-functional todo app using the use_map API: +```rust +static TodoList: FC<()> = |cx| { + let todos = use_map(cx, || HashMap::new()); + let input = use_ref(|| None); + + cx.render(rsx!{ + div { + button { + "Add todo" + onclick: move |_| { + let new_todo = TodoItem::new(input.contents()); + todos.insert(new_todo.id.clone(), new_todo); + input.clear(); + } + } + button { + "Clear todos" + onclick: move |_| todos.clear() + } + input { + placeholder: "What needs to be done?" + ref: input + } + ul { + {todos.iter().map(|todo| rsx!( + li { + key: todo.id + span { "{todo.content}" } + button {"x", onclick: move |_| todos.remove(todo.key.clone())} + } + ))} + } + } + }) +} + +``` + +*/ diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hooks/src/usesignal.rs b/packages/hooks/src/usesignal.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs new file mode 100644 index 000000000..5600636ca --- /dev/null +++ b/packages/hooks/src/usestate.rs @@ -0,0 +1,170 @@ +use dioxus_core::prelude::Context; +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + fmt::Display, + ops::{Deref, DerefMut}, + rc::Rc, +}; + +/// Store state between component renders! +/// +/// ## The "King" of state hooks +/// +/// The Dioxus version of `useState` is the "king daddy" of state management. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. +/// +/// [`use_state`] exposes a few helper methods to modify the underlying state: +/// - `.set(new)` allows you to override the "work in progress" value with a new value +/// - `.get_mut()` allows you to modify the WIP value +/// - `.get_wip()` allows you to access the WIP value +/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) +/// +/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations +/// will automatically be called on the WIP value. +/// +/// +/// Usage: +/// ```ignore +/// const Example: FC<()> = |cx| { +/// let counter = use_state(cx, || 0); +/// let increment = |_| counter += 1; +/// let decrement = |_| counter += 1; +/// +/// html! { +///

+///

"Counter: {counter}"

+/// +/// +///
+/// } +/// } +/// ``` +pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>( + cx: Context<'a, P>, + initial_state_fn: F, +) -> UseState { + cx.use_hook( + move || UseStateInner { + current_val: initial_state_fn(), + callback: cx.schedule_update(), + wip: RefCell::new(None), + update_scheuled: Cell::new(false), + }, + move |hook| { + hook.update_scheuled.set(false); + let mut new_val = hook.wip.borrow_mut(); + if new_val.is_some() { + hook.current_val = new_val.take().unwrap(); + } + + UseState { inner: &*hook } + }, + |_| {}, + ) +} +struct UseStateInner { + current_val: T, + update_scheuled: Cell, + callback: Rc, + wip: RefCell>, + updater: +} + +pub struct UseState<'a, T: 'static> { + inner: &'a UseStateInner, +} +impl Copy for UseState<'_, T> {} +impl<'a, T> Clone for UseState<'a, T> +where + T: 'static, +{ + fn clone(&self) -> Self { + UseState { inner: self.inner } + } +} + +impl<'a, T: 'static> UseState<'a, T> { + /// Tell the Dioxus Scheduler that we need to be processed + pub fn needs_update(&self) { + if !self.inner.update_scheuled.get() { + self.inner.update_scheuled.set(true); + (self.inner.callback)(); + } + } + + pub fn set(&self, new_val: T) { + self.needs_update(); + *self.inner.wip.borrow_mut() = Some(new_val); + } + + pub fn get(&self) -> &T { + &self.inner.current_val + } + + /// Get the current status of the work-in-progress data + pub fn get_wip(&self) -> Ref> { + self.inner.wip.borrow() + } + + pub fn classic(self) -> (&'a T, &'a Rc) { + (&self.inner.current_val) + } +} +impl<'a, T: 'static + ToOwned> UseState<'a, T> { + pub fn get_mut(self) -> RefMut<'a, T> { + // make sure we get processed + self.needs_update(); + + // Bring out the new value, cloning if it we need to + // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this + RefMut::map(self.inner.wip.borrow_mut(), |slot| { + if slot.is_none() { + *slot = Some(self.inner.current_val.to_owned()); + } + slot.as_mut().unwrap() + }) + } +} + +impl<'a, T: 'static> std::ops::Deref for UseState<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner.current_val + } +} + +use std::ops::{Add, AddAssign, Sub, SubAssign}; +impl<'a, T: Copy + Add> Add for UseState<'a, T> { + type Output = T; + + fn add(self, rhs: T) -> Self::Output { + self.inner.current_val.add(rhs) + } +} +impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { + fn add_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.add(rhs)); + } +} +impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { + type Output = T; + + fn sub(self, rhs: T) -> Self::Output { + self.inner.current_val.sub(rhs) + } +} +impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { + fn sub_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.sub(rhs)); + } +} + +// enable displaty for the handle +impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner.current_val) + } +} diff --git a/packages/hooks/src/usetask.rs b/packages/hooks/src/usetask.rs new file mode 100644 index 000000000..e69de29bb