diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs new file mode 100644 index 000000000..ab035f152 --- /dev/null +++ b/packages/fermi/src/hooks/state.rs @@ -0,0 +1,422 @@ +use crate::{use_atom_root, AtomId, AtomRoot, Readable, Writable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::{RefCell, RefMut}, + fmt::{Debug, Display}, + marker::PhantomData, + ops::{Add, Div, Mul, Not, Sub}, + rc::Rc, + sync::Arc, +}; + +/// Store state between component renders. +/// +/// ## Dioxus equivalent of AtomState, designed for Rust +/// +/// The Dioxus version of `AtomState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// +/// ```ignore +/// static COUNT: Atom = |_| 0; +/// +/// fn Example(cx: Scope) -> Element { +/// let mut count = use_atom_state(&cx, COUNT); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Count: {count}" } +/// button { onclick: move |_| count += 1, "Increment" } +/// button { onclick: move |_| count -= 1, "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_atom_state<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable) -> &'a AtomState { + let root = crate::use_atom_root(cx); + + let inner = cx.use_hook(|_| AtomState { + value: None, + root: root.clone(), + scope_id: cx.scope_id(), + id: f.unique_id(), + }); + + inner.value = Some(inner.root.register(f, cx.scope_id())); + + inner +} + +pub struct AtomState { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, +} + +impl Drop for AtomState { + fn drop(&mut self) { + self.root.unsubscribe(self.id, self.scope_id) + } +} + +impl AtomState { + /// Set the state to a new value. + pub fn set(&self, new: T) { + self.root.set(self.id, new) + } + + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let count = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.value.as_ref().unwrap().clone() + } + + /// Get the `setter` function directly without the `AtomState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`AtomState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: value.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + let root = self.root.clone(); + let id = self.id; + Rc::new(move |new_val| root.set(id, new_val)) + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// // to increment the value + /// value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let value = value.to_owned(); + /// async move { + /// value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + self.root.clone().set(self.id, { + let current = self.value.as_ref().unwrap(); + f(current.as_ref()) + }); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// let as_rc = value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &T { + self.value.as_ref().unwrap() + } + + #[must_use] + pub fn get_rc(&self) -> &Rc { + self.value.as_ref().unwrap() + } + + /// Mark all consumers of this atom to re-render + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let count = use_state(&cx, || 0); + /// cx.spawn({ + /// let count = count.to_owned(); + /// async move { + /// // for the component to re-render + /// count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + self.root.force_update(self.id) + } +} + +impl AtomState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let val = use_state(&cx, || 0); + /// + /// val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut new_val = self.value.as_ref().unwrap().as_ref().to_owned(); + apply(&mut new_val); + self.set(new_val); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let val = use_state(&cx, || 0); + /// + /// *val.make_mut() += 1; + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + todo!("make mut not support for atom values yet") + // let mut slot = self.value.as_ref().unwrap(); + + // self.needs_update(); + + // if Rc::strong_count(&*slot) > 0 { + // *slot = Rc::new(slot.as_ref().to_owned()); + // } + + // RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } + + /// Convert this handle to a tuple of the value and the handle itself. + #[must_use] + pub fn split(&self) -> (&T, &Self) { + (self.value.as_ref().unwrap(), self) + } +} + +impl Clone for AtomState { + fn clone(&self) -> Self { + AtomState { + root: self.root.clone(), + id: self.id, + scope_id: self.scope_id, + value: self.value.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value.as_ref().unwrap()) + } +} + +impl<'a, T: std::fmt::Binary> std::fmt::Binary for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:b}", self.value.as_ref().unwrap().as_ref()) + } +} + +impl PartialEq for AtomState { + fn eq(&self, other: &T) -> bool { + self.value.as_ref().unwrap().as_ref() == other + } +} + +// todo: this but for more interesting conrete types +impl PartialEq for &AtomState { + fn eq(&self, other: &bool) -> bool { + self.value.as_ref().unwrap().as_ref() == other + } +} + +impl PartialEq> for AtomState { + fn eq(&self, other: &AtomState) -> bool { + Rc::ptr_eq(self.value.as_ref().unwrap(), other.value.as_ref().unwrap()) + } +} + +impl Debug for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.value.as_ref().unwrap()) + } +} + +impl<'a, T> std::ops::Deref for AtomState { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.value.as_ref().unwrap().as_ref() + } +} + +impl std::ops::Not for &AtomState { + type Output = ::Output; + + fn not(self) -> Self::Output { + self.value.as_ref().unwrap().not() + } +} + +impl std::ops::Not for AtomState { + type Output = ::Output; + + fn not(self) -> Self::Output { + self.value.as_ref().unwrap().not() + } +} + +impl std::ops::Add for &AtomState { + type Output = ::Output; + + fn add(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() + other + } +} +impl std::ops::Sub for &AtomState { + type Output = ::Output; + + fn sub(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() - other + } +} + +impl std::ops::Div for &AtomState { + type Output = ::Output; + + fn div(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() / other + } +} + +impl std::ops::Mul for &AtomState { + type Output = ::Output; + + fn mul(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() * other + } +} + +impl + Copy> std::ops::AddAssign for &AtomState { + fn add_assign(&mut self, rhs: T) { + self.set((*self.current()) + rhs); + } +} + +impl + Copy> std::ops::SubAssign for &AtomState { + fn sub_assign(&mut self, rhs: T) { + self.set((*self.current()) - rhs); + } +} + +impl + Copy> std::ops::MulAssign for &AtomState { + fn mul_assign(&mut self, rhs: T) { + self.set((*self.current()) * rhs); + } +} + +impl + Copy> std::ops::DivAssign for &AtomState { + fn div_assign(&mut self, rhs: T) { + self.set((*self.current()) / rhs); + } +} + +impl + Copy> std::ops::AddAssign for AtomState { + fn add_assign(&mut self, rhs: T) { + self.set((*self.current()) + rhs); + } +} + +impl + Copy> std::ops::SubAssign for AtomState { + fn sub_assign(&mut self, rhs: T) { + self.set((*self.current()) - rhs); + } +} + +impl + Copy> std::ops::MulAssign for AtomState { + fn mul_assign(&mut self, rhs: T) { + self.set((*self.current()) * rhs); + } +} + +impl + Copy> std::ops::DivAssign for AtomState { + fn div_assign(&mut self, rhs: T) { + self.set((*self.current()) / rhs); + } +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index f8a835db2..4943e38a3 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -30,11 +30,13 @@ pub mod hooks { mod init_atom_root; mod read; mod set; + mod state; pub use atom_ref::*; pub use atom_root::*; pub use init_atom_root::*; pub use read::*; pub use set::*; + pub use state::*; } /// All Atoms are `Readable` - they support reading their value. diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index d80b176dc..c7c234ddb 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -39,8 +39,6 @@ impl AtomRoot { } pub fn register(&self, f: impl Readable, scope: ScopeId) -> Rc { - log::trace!("registering atom {:?}", f.unique_id()); - let mut atoms = self.atoms.borrow_mut(); // initialize the value if it's not already initialized @@ -97,7 +95,22 @@ impl AtomRoot { } } - pub fn read(&self, _f: impl Readable) -> &V { - todo!() + pub fn read(&self, f: impl Readable) -> Rc { + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers: HashSet::new(), + }, + ); + value + } } } diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 4adc357c4..5540f7b08 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -11,7 +11,7 @@ async fn main() { // todo: compactify this routing under one liveview::app method let view = liveview::new(addr); - let body = view.body(); + let body = view.body("Dioxus LiveView"); let routes = warp::path::end() .map(move || warp::reply::html(body.clone())) diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 948cffe5b..0c6f3e892 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -65,8 +65,10 @@ pub async fn connect(ws: WebSocket, pool: LocalPoolHandle, app: fn(Scope) -> Ele Either::Left((l, _)) => { if let Some(Ok(msg)) = l { if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); + if msg.method == "user_event" { + let user_event = events::trigger_from_serialized(msg.params); + event_tx.send(user_event).unwrap(); + } } else { break; } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 340a3e4ea..f3ad90511 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -31,14 +31,13 @@ pub struct Liveview { } impl Liveview { - pub fn body(&self) -> String { + pub fn body(&self, header: &str) -> String { format!( r#" - Dioxus app - + {header}
diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index cfb1b0447..a37c6f102 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -51,7 +51,7 @@ impl UseRoute { #[cfg(feature = "query")] pub fn query(&self) -> Option { let query = self.url().query()?; - serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or("")).ok() + serde_urlencoded::from_str(query).ok() } /// Get the first query parameter given the parameter name.