diff --git a/Cargo.toml b/Cargo.toml index af5e8a733..7d8cc6871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "packages/native-core", "packages/native-core-macro", "packages/rsx-rosetta", + "packages/signals", "docs/guide", ] @@ -43,6 +44,7 @@ dioxus = { path = "./packages/dioxus" } dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] } dioxus-ssr = { path = "./packages/ssr" } dioxus-router = { path = "./packages/router" } +dioxus-signals = { path = "./packages/signals" } fermi = { path = "./packages/fermi" } futures-util = "0.3.21" log = "0.4.14" diff --git a/examples/signals.rs b/examples/signals.rs new file mode 100644 index 000000000..988b9af41 --- /dev/null +++ b/examples/signals.rs @@ -0,0 +1,30 @@ +use dioxus::prelude::*; +use dioxus_signals::{use_init_signal_rt, use_signal}; +use std::time::Duration; + +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 { + loop { + count += 1; + tokio::time::sleep(Duration::from_millis(400)).await; + } + }); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + + if count() > 5 { + rsx!{ h2 { "High five!" } } + } + }) +} diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml new file mode 100644 index 000000000..9a12c787d --- /dev/null +++ b/packages/signals/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dioxus-signals" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core" } +slab = "0.4.7" diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs new file mode 100644 index 000000000..e4b499ed8 --- /dev/null +++ b/packages/signals/src/lib.rs @@ -0,0 +1,131 @@ +use std::{ + cell::{Ref, RefMut}, + fmt::Display, + marker::PhantomData, + ops::{Add, Div, Mul, Sub}, +}; + +mod rt; + +use dioxus_core::ScopeState; +pub use rt::*; + +pub fn use_init_signal_rt(cx: &ScopeState) { + cx.use_hook(|| { + let rt = crate::rt::claim_rt(cx.schedule_update_any()); + cx.provide_context(rt); + }); +} + +pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { + cx.use_hook(|| { + let rt: &'static SignalRt = cx.consume_context().unwrap(); + let id = rt.init(f()); + rt.subscribe(id, cx.scope_id()); + + struct SignalHook { + signal: Signal, + } + + impl Drop for SignalHook { + fn drop(&mut self) { + self.signal.rt.remove(self.signal.id); + } + } + + SignalHook { + signal: Signal { + id, + rt, + t: PhantomData, + }, + } + }) + .signal +} + +pub struct Signal { + id: usize, + rt: &'static SignalRt, + t: PhantomData, +} + +impl Signal { + pub fn read(&self) -> Ref { + self.rt.read(self.id) + } + + pub fn write(&self) -> RefMut { + self.rt.write(self.id) + } + + pub fn set(&mut self, value: T) { + self.rt.set(self.id, value); + } + + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + let write = self.read(); + f(&*write) + } + + pub fn update(&self, _f: impl FnOnce(&mut T) -> O) -> O { + let mut write = self.write(); + _f(&mut *write) + } +} + +impl Signal { + pub fn get(&self) -> T { + self.rt.get(self.id) + } +} + +impl std::ops::Deref for Signal { + type Target = dyn Fn() -> T; + + fn deref(&self) -> &Self::Target { + self.rt.getter(self.id) + } +} + +impl std::clone::Clone for Signal { + fn clone(&self) -> Self { + Self { + t: PhantomData, + id: self.id, + rt: self.rt, + } + } +} + +impl Copy for Signal {} + +impl Display for Signal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.rt.with::(self.id, |v| T::fmt(v, f)) + } +} + +impl + Copy + 'static> std::ops::AddAssign for Signal { + fn add_assign(&mut self, rhs: T) { + self.set(self.get() + rhs); + } +} + +impl + Copy + 'static> std::ops::SubAssign for Signal { + fn sub_assign(&mut self, rhs: T) { + self.set(self.get() - rhs); + } +} + +impl + Copy + 'static> std::ops::MulAssign for Signal { + fn mul_assign(&mut self, rhs: T) { + self.set(self.get() * rhs); + } +} + +impl + Copy + 'static> std::ops::DivAssign for Signal { + fn div_assign(&mut self, rhs: T) { + self.set(self.get() / rhs); + } +} diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs new file mode 100644 index 000000000..e849f9008 --- /dev/null +++ b/packages/signals/src/rt.rs @@ -0,0 +1,121 @@ +use std::{any::Any, cell::RefCell, sync::Arc}; + +use dioxus_core::ScopeId; +use slab::Slab; + +thread_local! { + // we cannot drop these since any future might be using them + static RUNTIMES: RefCell> = RefCell::new(Vec::new()); +} + +/// 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; + } + + 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); + }); +} + +pub struct SignalRt { + pub(crate) signals: RefCell>, + pub(crate) update_any: Arc, +} + +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 fn subscribe(&self, id: usize, subscriber: ScopeId) { + self.signals.borrow_mut()[id].subscribers.push(subscriber); + } + + pub fn get(&self, id: usize) -> T { + self.signals.borrow()[id] + .value + .downcast_ref::() + .cloned() + .unwrap() + } + + pub fn set(&self, id: usize, value: T) { + let mut signals = self.signals.borrow_mut(); + let inner = &mut signals[id]; + inner.value = Box::new(value); + + for subscriber in inner.subscribers.iter() { + (self.update_any)(*subscriber); + } + } + + pub fn remove(&self, id: usize) { + self.signals.borrow_mut().remove(id); + } + + pub fn with(&self, id: usize, f: impl FnOnce(&T) -> O) -> O { + let signals = self.signals.borrow(); + let inner = &signals[id]; + let inner = inner.value.downcast_ref::().unwrap(); + f(inner) + } + + pub(crate) fn read(&self, id: usize) -> std::cell::Ref { + let signals = self.signals.borrow(); + std::cell::Ref::map(signals, |signals| { + signals[id].value.downcast_ref::().unwrap() + }) + } + + pub(crate) fn write(&self, id: usize) -> std::cell::RefMut { + let signals = self.signals.borrow_mut(); + std::cell::RefMut::map(signals, |signals| { + signals[id].value.downcast_mut::().unwrap() + }) + } + + pub(crate) fn getter(&self, id: usize) -> &dyn Fn() -> T { + let mut signals = self.signals.borrow_mut(); + let inner = &mut signals[id]; + let r = inner.getter.as_mut(); + + if r.is_none() { + let rt = self; + let r = move || rt.get::(id); + let getter: Box T> = Box::new(r); + let getter: Box = unsafe { std::mem::transmute(getter) }; + + inner.getter = Some(getter); + } + + let r = inner.getter.as_ref().unwrap(); + + unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) } + } +} + +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>, +}