Files
dioxus/packages/atoms/src/lib.rs
T

574 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{
cell::{Ref, RefCell},
collections::HashMap,
hash::Hash,
marker::PhantomData,
rc::Rc,
};
pub use atomfamily::*;
pub use atoms::*;
pub use ecs::*;
use error::*;
pub use hooks::*;
pub use hooks::*;
pub use root::*;
pub use selector::*;
pub use selectorfamily::*;
pub use traits::*;
pub use utils::*;
mod tracingimmap;
mod traits {
use dioxus_core::prelude::Context;
use super::*;
pub trait MapKey: PartialEq + Hash + 'static {}
impl<T: PartialEq + Hash + 'static> MapKey for T {}
pub trait AtomValue: PartialEq + 'static {}
impl<T: PartialEq + 'static> AtomValue for T {}
// Atoms, selectors, and their family variants are readable
pub trait Readable<T: AtomValue>: Sized + Copy {
fn use_read<'a, P: 'static>(self, cx: Context<'a, P>) -> &'a T {
hooks::use_read(cx, self)
}
// This returns a future of the value
// If the atom is currently pending, that future will resolve to pending
// If the atom is currently ready, the future will immediately resolve
// if the atom switches from ready to pending, the component will re-run, returning a pending future
fn use_read_async<'a, P>(self, cx: Context<'a, P>) -> &'a T {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> T;
// We use the Raw Ptr to the atom
// TODO: Make sure atoms with the same definitions don't get merged together. I don't think they do, but double check
fn static_id(self) -> u32;
}
// Only atoms and atom families are writable
// Selectors and selector families are not
pub trait Writable<T: AtomValue>: Readable<T> + Sized {
fn use_read_write<'a, P>(self, cx: Context<'a, P>) -> (&'a T, &'a Rc<dyn Fn(T)>) {
todo!()
}
fn use_write<'a, P>(self, cx: Context<'a, P>) -> &'a Rc<dyn Fn(T)> {
todo!()
}
}
}
mod atoms {
use super::*;
// Currently doesn't do anything, but will eventually add effects, id, serialize/deserialize keys, etc
// Doesn't currently allow async values, but the goal is to eventually enable them
pub struct AtomBuilder {}
pub type Atom<T> = fn(&mut AtomBuilder) -> T;
// impl<T: AtomValue> Readable<T> for Atom<T> {}
impl<T: AtomValue> Readable<T> for &'static Atom<T> {
fn static_id(self) -> u32 {
self as *const _ as u32
}
fn initialize(self, api: &RecoilRoot) -> T {
let mut builder = AtomBuilder {};
let p = self(&mut builder);
p
}
}
impl<T: AtomValue> Writable<T> for &'static Atom<T> {}
mod compilests {
use super::*;
use dioxus_core::prelude::Context;
fn _test(cx: Context<()>) {
const EXAMPLE_ATOM: Atom<i32> = |_| 10;
// ensure that atoms are both read and write
let _ = use_read(cx, &EXAMPLE_ATOM);
let _ = use_read_write(cx, &EXAMPLE_ATOM);
let _ = use_write(cx, &EXAMPLE_ATOM);
}
}
}
mod atomfamily {
use super::*;
pub trait FamilyCollection<K, V> {}
impl<K, V> FamilyCollection<K, V> for HashMap<K, V> {}
use im_rc::HashMap as ImHashMap;
/// AtomHashMaps provide an efficient way of maintaing collections of atoms.
///
/// Under the hood, AtomHashMaps uses [IM](https://www.rust-lang.org)'s immutable HashMap implementation to lazily
/// clone data as it is modified.
///
///
///
///
///
///
pub type AtomHashMap<K, V> = fn(&mut ImHashMap<K, V>);
pub trait AtomFamilySelector<K: MapKey, V: AtomValue + Clone> {
fn select(&'static self, k: &K) -> AtomMapSelection<K, V> {
todo!()
}
}
impl<K: MapKey, V: AtomValue + Clone> AtomFamilySelector<K, V> for AtomHashMap<K, V> {
fn select(&'static self, k: &K) -> AtomMapSelection<K, V> {
todo!()
}
}
pub struct AtomMapSelection<'a, K: MapKey, V: AtomValue> {
root: &'static AtomHashMap<K, V>,
key: &'a K,
}
impl<'a, K: MapKey, V: AtomValue> Readable<V> for &AtomMapSelection<'a, K, V> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> V {
todo!()
// let mut builder = AtomBuilder {};
// let p = self(&mut builder);
// p
}
}
impl<'a, K: MapKey, T: AtomValue> Writable<T> for &AtomMapSelection<'a, K, T> {}
mod compiletests {
use dioxus_core::prelude::Context;
use super::*;
const Titles: AtomHashMap<u32, &str> = |map| {};
fn test(cx: Context<()>) {
let title = Titles.select(&10).use_read(cx);
let t2 = use_read(cx, &Titles.select(&10));
}
}
}
mod selector {
use super::*;
pub struct SelectorBuilder {}
impl SelectorBuilder {
pub fn get<T: AtomValue>(&self, t: impl Readable<T>) -> &T {
todo!()
}
}
pub type Selector<T> = fn(&mut SelectorBuilder) -> T;
impl<T: AtomValue> Readable<T> for &'static Selector<T> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> T {
todo!()
}
}
pub struct SelectorFamilyBuilder {}
impl SelectorFamilyBuilder {
pub fn get<T: AtomValue>(&self, t: impl Readable<T>) -> &T {
todo!()
}
}
}
mod selectorfamily {
use super::*;
// pub trait SelectionSelector<K, V> {
// fn select(&self, k: &K) -> CollectionSelection<K, V> {
// todo!()
// }
// }
// impl<K, V, F> SelectionSelector<K, V> for AtomFamily<K, V, F> {}
/// Create a new value as a result of a combination of previous values
/// If you need to return borrowed data, check out [`SelectorFamilyBorrowed`]
pub type SelectorFamily<Key, Value> = fn(&mut SelectorFamilyBuilder, Key) -> Value;
impl<K, V: AtomValue> Readable<V> for &'static SelectorFamily<K, V> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> V {
todo!()
}
}
/// Borrowed selector families are – surprisingly - discouraged.
/// This is because it's not possible safely memoize these values without keeping old versions around.
///
/// However, it does come in handy to borrow the contents of an item without re-rendering child components.
pub type SelectorFamilyBorrowed<Key, Value> =
for<'a> fn(&'a mut SelectorFamilyBuilder, Key) -> &'a Value;
// impl<'a, K, V: 'a> SelectionSelector<K, V> for fn(&'a mut SelectorFamilyBuilder, K) -> V {}
}
mod root {
use std::{
any::{Any, TypeId},
collections::{HashSet, VecDeque},
iter::FromIterator,
sync::atomic::{AtomicU32, AtomicUsize},
};
use super::*;
// use generational_arena::Index as ConsumerId;
type AtomId = u32;
type ConsumerId = u32;
pub type RecoilContext = RefCell<RecoilRoot>;
// Sometimes memoization means we don't need to re-render components that holds "correct values"
// IE we consider re-render more expensive than keeping the old value around.
// We *could* unsafely overwrite this slot, but that's just **asking** for UB (holding a &mut while & is held in components)
//
// Instead, we choose to let the hook itself hold onto the Rc<T> by not forcing a render when T is the same.
// Whenever the component needs to be re-rendered for other reasons, the "get" method will automatically update the Rc<T> to the most recent one.
pub struct RecoilRoot {
nodes: RefCell<HashMap<AtomId, Slot>>,
consumer_map: HashMap<ConsumerId, AtomId>,
}
struct Slot {
type_id: TypeId,
source: AtomId,
value: Rc<dyn Any>,
consumers: HashMap<ConsumerId, Rc<dyn Fn()>>,
dependents: HashSet<AtomId>,
}
static NEXT_ID: AtomicU32 = AtomicU32::new(0);
fn next_consumer_id() -> u32 {
NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
impl RecoilRoot {
pub(crate) fn new() -> Self {
Self {
nodes: Default::default(),
consumer_map: Default::default(),
}
}
pub fn subscribe<T: AtomValue>(
&mut self,
readable: impl Readable<T>,
receiver_fn: Rc<dyn Fn()>,
) -> ConsumerId {
let consumer_id = next_consumer_id();
let atom_id = readable.static_id();
log::debug!("Subscribing consumer to atom {} {}", consumer_id, atom_id);
let mut nodes = self.nodes.borrow_mut();
let slot = nodes.get_mut(&atom_id).unwrap();
slot.consumers.insert(consumer_id, receiver_fn);
self.consumer_map.insert(consumer_id, atom_id);
consumer_id
}
pub fn unsubscribe(&mut self, consumer_id: ConsumerId) {
let atom_id = self.consumer_map.get(&consumer_id).unwrap();
let mut nodes = self.nodes.borrow_mut();
let slot = nodes.get_mut(&atom_id).unwrap();
slot.consumers.remove(&consumer_id);
}
/// Directly get the *slot*
/// All Atoms are held in slots (an Rc)
///
///
pub fn try_get_raw<T: AtomValue>(&self, readable: impl Readable<T>) -> Result<Rc<T>> {
let atom_id = readable.static_id();
let mut nodes = self.nodes.borrow_mut();
if !nodes.contains_key(&atom_id) {
let value = Slot {
type_id: TypeId::of::<T>(),
source: atom_id,
value: Rc::new(readable.initialize(self)),
consumers: Default::default(),
dependents: Default::default(),
};
nodes.insert(atom_id, value);
}
let out = nodes
.get(&atom_id)
.unwrap()
.value
.clone()
.downcast::<T>()
.unwrap();
Ok(out)
}
pub fn try_set<T: AtomValue>(
&mut self,
writable: impl Writable<T>,
new_val: T,
) -> crate::error::Result<()> {
let atom_id = writable.static_id();
self.set_by_id(atom_id, new_val);
Ok(())
}
// A slightly dangerous method to manually overwrite any slot given an AtomId
pub(crate) fn set_by_id<T: AtomValue>(&mut self, atom_id: AtomId, new_val: T) {
let mut nodes = self.nodes.borrow_mut();
let consumers = match nodes.get_mut(&atom_id) {
Some(slot) => {
slot.value = Rc::new(new_val);
&slot.consumers
}
None => {
let value = Slot {
type_id: TypeId::of::<T>(),
source: atom_id,
value: Rc::new(new_val),
// value: Rc::new(writable.initialize(self)),
consumers: Default::default(),
dependents: Default::default(),
};
nodes.insert(atom_id, value);
&nodes.get(&atom_id).unwrap().consumers
}
};
for (id, consumer_fn) in consumers {
log::debug!("triggering selector {}", id);
consumer_fn();
}
}
}
}
mod hooks {
use super::*;
use dioxus_core::prelude::Context;
pub fn use_init_recoil_root<P>(cx: Context<P>, cfg: impl Fn(())) {
cx.use_create_context(move || RefCell::new(RecoilRoot::new()))
}
/// Gain access to the recoil API directly - set, get, modify, everything
/// This is the foundational hook in which read/write/modify are built on
///
/// This does not subscribe the component to *any* updates
///
/// You can use this method to create controllers that perform much more complex actions than set/get
/// However, be aware that "getting" values through this hook will not subscribe the component to any updates.
pub fn use_recoil_api<'a, P>(cx: Context<'a, P>) -> &'a Rc<RecoilContext> {
cx.use_context::<RecoilContext>()
}
pub fn use_write<'a, P, T: AtomValue>(
cx: Context<'a, P>,
// todo: this shouldn't need to be static
writable: impl Writable<T>,
) -> &'a Rc<dyn Fn(T)> {
let api = use_recoil_api(cx);
cx.use_hook(
move |_| {
let api = api.clone();
let raw_id = writable.static_id();
Rc::new(move |new_val| {
//
log::debug!("setting new value ");
let mut api = api.as_ref().borrow_mut();
// api.try_set(writable, new_val).expect("failed to set");
api.set_by_id(raw_id, new_val);
}) as Rc<dyn Fn(T)>
},
move |hook| &*hook,
|hook| {},
)
}
/// Read the atom and get the Rc directly to the Atom's slot
/// This is useful if you need the memoized Atom value. However, Rc<T> is not as easy to
/// work with as
pub fn use_read_raw<'a, P, T: AtomValue>(
cx: Context<'a, P>,
readable: impl Readable<T>,
) -> &'a Rc<T> {
struct ReadHook<T> {
value: Rc<T>,
consumer_id: u32,
}
let api = use_recoil_api(cx);
cx.use_hook(
move |_| {
let mut api = api.as_ref().borrow_mut();
let update = cx.schedule_update();
let val = api.try_get_raw(readable).unwrap();
let id = api.subscribe(readable, update);
ReadHook {
value: val,
consumer_id: id,
}
},
move |hook| {
let api = api.as_ref().borrow();
let val = api.try_get_raw(readable).unwrap();
hook.value = val;
&hook.value
},
move |hook| {
let mut api = api.as_ref().borrow_mut();
api.unsubscribe(hook.consumer_id);
},
)
}
///
pub fn use_read<'a, P, T: AtomValue>(cx: Context<'a, P>, readable: impl Readable<T>) -> &'a T {
use_read_raw(cx, readable).as_ref()
}
/// # Use an atom in both read and write
///
/// This method is only available for atoms and family selections (not selectors).
///
/// This is equivalent to calling both `use_read` and `use_write`, but saves you the hassle and repitition
///
/// ## Example
///
/// ```
/// const Title: Atom<&str> = |_| "hello";
/// let (title, set_title) = use_read_write(cx, &Title);
///
/// // equivalent to:
/// let (title, set_title) = (use_read(cx, &Title), use_write(cx, &Title));
/// ```
pub fn use_read_write<'a, P, T: AtomValue + 'static>(
cx: Context<'a, P>,
writable: impl Writable<T>,
) -> (&'a T, &'a Rc<dyn Fn(T)>) {
(use_read(cx, writable), use_write(cx, writable))
}
/// # Modify an atom without using `use_read`.
///
/// Occasionally, a component might want to write to an atom without subscribing to its changes. `use_write` does not
/// provide this functionality, so `use_modify` exists to gain access to the current atom value while setting it.
///
/// ## Notes
///
/// Do note that this hook can only be used with Atoms where T: Clone since we actually clone the current atom to make
/// it mutable.
///
/// Also note that you need to stack-borrow the closure since the modify closure expects an &dyn Fn. If we made it
/// a static type, it wouldn't be possible to use the `modify` closure more than once (two closures always are different)
///
/// ## Example
///
/// ```ignore
/// let modify_atom = use_modify(cx, Atom);
///
/// modify_atom(&|a| *a += 1)
/// ```
pub fn use_modify<'a, T: AtomValue + 'static + Clone, P>(
cx: Context<'a, P>,
writable: impl Writable<T>,
) -> impl Fn(&dyn Fn()) {
|_| {}
}
/// Use a family collection directly
/// !! Any changes to the family will cause this subscriber to update
/// Try not to put this at the very top-level of your app.
pub fn use_read_family<'a, K, V, P>(
cx: Context<'a, P>,
t: &AtomHashMap<K, V>,
) -> &'a im_rc::HashMap<K, V> {
todo!()
}
}
mod ecs {
use super::*;
pub struct Blah<K, V> {
_p: PhantomData<(K, V)>,
}
pub type EcsModel<K, Ty> = fn(Blah<K, Ty>);
}
mod utils {
use super::*;
use dioxus_core::prelude::*;
pub struct RecoilApi {}
/// This tiny util wraps your main component with the initializer for the recoil root.
/// This is useful for small programs and the examples in this crate
pub fn RecoilApp<T: 'static>(
root: impl for<'a> Fn(Context<'a, T>) -> VNode,
) -> impl for<'a> Fn(Context<'a, T>) -> VNode {
move |cx| {
use_init_recoil_root(cx, |_| {});
root(cx)
}
}
}
mod compiletests {}
pub mod error {
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
#[error("Fatal Internal Error: {0}")]
FatalInternal(&'static str),
// #[error("Context is missing")]
// MissingSharedContext,
// #[error("No event to progress")]
// NoEvent,
// #[error("Wrong Properties Type")]
// WrongProps,
// #[error("Base scope has not been mounted yet")]
// NotMounted,
// #[error("I/O Error: {0}")]
// BorrowMut(#[from] std::),
// #[error("eee Error: {0}")]
// IO(#[from] core::result::),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error
}
}