wip: recoil
This commit is contained in:
1 parent
3cfa1fe125
commit
ee67654f58
31 files changed
+1369
-235
No files matched your search
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
@@ -9,3 +9,7 @@ edition = "2018"
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core" }
|
||||
uuid = "0.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-web = { path = "../web" }
|
||||
wasm-bindgen-futures = "*"
|
||||
@@ -1,6 +1,6 @@
|
||||
# Recoil.rs - Official global state management solution for Dioxus Apps
|
||||
|
||||
Recoil.rs provides a global state management API for Dioxus apps built on the concept of "atomic state." Instead of grouping state together into a single bundle ALA Redux, Recoil provides individual building blocks of state called Atoms. These atoms can be set/get anywhere in the app and combined to craft complex state. Recoil should be easier to learn and more efficient than Redux. Recoil.rs is modeled after the Recoil.JS project and pulls in
|
||||
Recoil.rs provides a global state management API for Dioxus apps built on the concept of "atomic state." Instead of grouping state together into a single bundle ALA Redux, Recoil provides individual building blocks of state called Atoms. These atoms can be set/get anywhere in the app and combined to craft complex state. Recoil should be easier to learn and more efficient than Redux. Recoil.rs is modeled after the Recoil.JS project.
|
||||
|
||||
Recoil.rs is officially supported by the Dioxus team. By doing so, are are "planting our flag in the stand" for atomic state management instead of bundled (Redux-style) state management. Atomic state management fits well with the internals of Dioxus, meaning Recoil.rs state management will be faster, more efficient, and less sensitive to data races than Redux-style apps.
|
||||
|
||||
@@ -8,10 +8,10 @@ Internally, Dioxus uses batching to speed up linear-style operations. Recoil.rs
|
||||
|
||||
## Guide
|
||||
|
||||
A simple atom of state is defined globally as a static:
|
||||
A simple atom of state is defined globally as a const:
|
||||
|
||||
```rust
|
||||
const Light: Atom<&'static str> = atom(|_| "Green");
|
||||
const Light: Atom<&'static str> = |_| "Green";
|
||||
```
|
||||
|
||||
This atom of state is initialized with a value of `"Green"`. The atom that is returned does not actually contain any values. Instead, the atom's key - which is automatically generated in this instance - is used in the context of a Recoil App.
|
||||
@@ -20,7 +20,7 @@ This is then later used in components like so:
|
||||
|
||||
```rust
|
||||
fn App(ctx: Context, props: &()) -> DomTree {
|
||||
// The recoil root must be initialized at the top of the application before any use_recoils
|
||||
// The recoil root must be initialized at the top of the application before any use_recoil hooks
|
||||
recoil::init_recoil_root(&ctx, |_| {});
|
||||
|
||||
let color = use_recoil(&ctx, &TITLE);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//! RecoilAPI Pattern
|
||||
//! ----------------
|
||||
//! This example demonstrates how the use_recoil_callback hook can be used to build view controllers.
|
||||
//! These view controllers are cheap to make and can be easily shared across the app to provide global
|
||||
//! state logic to many components.
|
||||
//!
|
||||
//! This pattern is meant to replace the typical use_dispatch pattern used in Redux apps.
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const TITLE: Atom<String> = |_| format!("Heading");
|
||||
const SUBTITLE: Atom<String> = |_| format!("Subheading");
|
||||
|
||||
struct TitleController(RecoilApi);
|
||||
impl TitleController {
|
||||
fn new(api: RecoilApi) -> Self {
|
||||
Self(api)
|
||||
}
|
||||
fn uppercase(&self) {
|
||||
self.0.modify(&TITLE, |f| *f = f.to_uppercase());
|
||||
self.0.modify(&SUBTITLE, |f| *f = f.to_uppercase());
|
||||
}
|
||||
fn lowercase(&self) {
|
||||
self.0.modify(&TITLE, |f| *f = f.to_lowercase());
|
||||
self.0.modify(&SUBTITLE, |f| *f = f.to_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(|ctx, _| {
|
||||
let title = use_recoil_value(ctx, &TITLE);
|
||||
let subtitle = use_recoil_value(ctx, &SUBTITLE);
|
||||
let controller = use_recoil_callback(ctx, TitleController::new);
|
||||
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"{title}"
|
||||
"{subtitle}"
|
||||
button { onclick: move |_| controller.uppercase(), "Uppercase" }
|
||||
button { onclick: move |_| controller.lowercase(), "Lowercase" }
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! Example: RecoilCallback
|
||||
//! ------------------------
|
||||
//! This example shows how use_recoil_callback can be used to abstract over sets/gets.
|
||||
//! This hook provides a way to capture the RecoilApi object. In this case, we capture
|
||||
//! it in a closure an abstract the set/get functionality behind the update_title function.
|
||||
//!
|
||||
//! It should be straightforward to build a complex app with recoil_callback.
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const TITLE: Atom<&str> = |_| "red";
|
||||
|
||||
fn update_title(api: &RecoilApi) {
|
||||
match *api.get(&TITLE) {
|
||||
"green" => api.set(&TITLE, "yellow"),
|
||||
"yellow" => api.set(&TITLE, "red"),
|
||||
"red" => api.set(&TITLE, "green"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
let title = use_recoil_value(ctx, &TITLE);
|
||||
let next_light = use_recoil_callback(ctx, |api| move |_| update_title(&api));
|
||||
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"{title}"
|
||||
button { onclick: {next_light}, "Next light" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Example: ECS Architecture for very list selectors
|
||||
//! --------------------------------------------------
|
||||
//! Sometimes, you need *peak* performance. Cloning and Rc might simply be too much overhead for your app.
|
||||
//! If you're building CPU intense apps like graphics editors, simulations, or advanced visualizations,
|
||||
//! slicing up your state beyond atoms might be desirable.
|
||||
//!
|
||||
//! Instead of storing groups of entities in a collection of structs, the ECS Architecture instead stores
|
||||
//! an array for each entity in the collection. This tends to improve performance for batch operations on
|
||||
//! individual fields at the cost of complexity. Fortunately, this ECS model is built right into Recoil,
|
||||
//! making it easier than ever to enable sharded datastructures in your app.
|
||||
//!
|
||||
//! Instead of defining a struct for our primary datastructure, we'll instead use a type tuple, and then later
|
||||
//! index that tuple to get the value we care about. Unfortunately, we lose name information wrt to each
|
||||
//! type in the type tuple. This can be solved with an associated module, the derive EcsMacro, or just
|
||||
//! by good documentation.
|
||||
//!
|
||||
//! This approach is best suited for applications where individual entries in families are very large
|
||||
//! and updates to neighbors are costly in terms of Clone or field comparisons for memoization.
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use recoil::*;
|
||||
|
||||
type TodoModel = (
|
||||
bool, // checked
|
||||
String, // name
|
||||
String, // contents
|
||||
);
|
||||
|
||||
const TODOS: EcsModel<u32, TodoModel> = |builder| {};
|
||||
// const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(0).select(k);
|
||||
// const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(1).select(k);
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
// let title = use_recoil_value(ctx, &C_SELECTOR);
|
||||
dbg!(TODOS);
|
||||
|
||||
let title = "";
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"{title}"
|
||||
// button { onclick: {next_light}, "Next light" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const TODOS: AtomFamily<&str, Todo> = |_| HashMap::new();
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Todo {
|
||||
checked: bool,
|
||||
contents: String,
|
||||
}
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"Basic Todolist with AtomFamilies in Recoil.rs"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Child: FC<()> = |ctx, _| {
|
||||
// let todo = use_recoil_value(ctx, &TODOS);
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const COUNT: Atom<i32> = |_| 0;
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
let (count, set_count) = use_recoil_state(ctx, &COUNT);
|
||||
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"Count: {count}"
|
||||
button { onclick: move |_| set_count(count + 1), "Incr" }
|
||||
button { onclick: move |_| set_count(count - 1), "Decr" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const A_ITEMS: AtomFamily<i32, i32> = |_| HashMap::new();
|
||||
const B_ITEMS: AtomFamily<i32, i32> = |_| HashMap::new();
|
||||
|
||||
const C_SELECTOR: SelectorFamily<i32, i32> = |api, key| {
|
||||
let a = api.get(&A_ITEMS.select(&key));
|
||||
let b = api.get(&B_ITEMS.select(&key));
|
||||
a + b
|
||||
};
|
||||
|
||||
const D_SELECTOR: SelectorFamilyBorrowed<i32, i32> = |api, key| -> &i32 {
|
||||
let a = api.get(&A_ITEMS.select(&key));
|
||||
a
|
||||
};
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
let title = use_recoil_value(ctx, &C_SELECTOR);
|
||||
|
||||
let title = "";
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
"{title}"
|
||||
// button { onclick: {next_light}, "Next light" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use dioxus_core::prelude::*;
|
||||
use recoil::*;
|
||||
|
||||
const A: Atom<i32> = |_| 0;
|
||||
const B: Atom<i32> = |_| 0;
|
||||
const C: Selector<i32> = |api| api.get(&A) + api.get(&B);
|
||||
|
||||
static App: FC<()> = |ctx, _| {
|
||||
use_init_recoil_root(ctx);
|
||||
rsx! { in ctx,
|
||||
div {
|
||||
Banner {}
|
||||
BtnA {}
|
||||
BtnB {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Banner: FC<()> = |ctx, _| {
|
||||
let count = use_recoil_value(ctx, &C);
|
||||
ctx.render(rsx! { h1 { "Count: {count}" } })
|
||||
};
|
||||
|
||||
static BtnA: FC<()> = |ctx, _| {
|
||||
let (a, set) = use_recoil_state(ctx, &A);
|
||||
rsx! { in ctx,
|
||||
div { "a"
|
||||
button { "+", onclick: move |_| set(a + 1) }
|
||||
button { "-", onclick: move |_| set(a - 1) }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static BtnB: FC<()> = |ctx, _| {
|
||||
let (b, set) = use_recoil_state(ctx, &B);
|
||||
rsx! { in ctx,
|
||||
div { "b"
|
||||
button { "+", onclick: move |_| set(b + 1) }
|
||||
button { "-", onclick: move |_| set(b - 1) }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn main() {
|
||||
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Architecture
|
||||
|
||||
|
||||
|
||||
|
||||
## ECS
|
||||
It's often ideal to represent list-y state as an SoA (struct of arrays) instead of an AoS (array of structs). In 99% of apps, normal clone-y performance is fine. If you need more performance on top of cloning on update, IM.rs will provide fast immutable data structures.
|
||||
|
||||
But, if you need **extreme performance** consider the ECS (SoA) model. With ECS model, we can modify fields of an entry without invalidating selectors on neighboring fields.
|
||||
|
||||
This approach is for that 0.1% of apps that need peak performance. Our philosophy is that these tools should be available when needed, but you shouldn't need to reach for them often. An example use case might be a graphics editor or simulation engine where thousands of entities with many fields are rendered to the screen in realtime.
|
||||
|
||||
Recoil will even help you:
|
||||
```rust
|
||||
type TodoModel = (
|
||||
String, // title
|
||||
String // subtitle
|
||||
);
|
||||
const TODOS: RecoilEcs<u32, TodoModel> = |builder| {
|
||||
builder.push("SomeTitle".to_string(), "SomeSubtitle".to_string());
|
||||
};
|
||||
const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(0).select(k);
|
||||
const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(1).select(k);
|
||||
```
|
||||
|
||||
Or with a custom derive macro to take care of some boilerplate and maintain readability. This macro simply generates the type tuple from the model fields and then some associated constants for indexing them.
|
||||
```rust
|
||||
#[derive(EcsModel)]
|
||||
struct TodoModel {
|
||||
title: String,
|
||||
subtitle: String
|
||||
}
|
||||
|
||||
// derives these impl (you don't need to write this yourself, but can if you want):
|
||||
mod TodoModel {
|
||||
type Layout = (String, String);
|
||||
const title: u8 = 1;
|
||||
const subtitle: u8 = 2;
|
||||
}
|
||||
|
||||
|
||||
const TODOS: RecoilEcs<u32, TodoModel::Layout> = |builder| {};
|
||||
const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(TodoModel::title).select(k);
|
||||
const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(TodoModel::subtitle).select(k);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Optimization
|
||||
|
||||
Selectors and references.
|
||||
----
|
||||
Because new values are inserted without touching the original, we can keep old values around. As such, it makes sense to allow borrowed data since we can continue to reference old data if the data itself hasn't changed.
|
||||
|
||||
However, this does lead to a greater memory overhead, so occasionally we'll want to sacrifice an a component render in order to evict old values.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::{AtomValue, Readable, RecoilItem};
|
||||
|
||||
pub type Atom<T: PartialEq> = fn(&mut AtomBuilder<T>) -> T;
|
||||
|
||||
impl<T: AtomValue + 'static> Readable<T> for Atom<T> {
|
||||
fn load(&'static self) -> RecoilItem {
|
||||
todo!()
|
||||
// RecoilItem::Atom(self as *const _ as _)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AtomBuilder<T: PartialEq> {
|
||||
pub key: String,
|
||||
_never: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq> AtomBuilder<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
key: "".to_string(),
|
||||
_never: std::marker::PhantomData {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, _key: &'static str) {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Atom;
|
||||
|
||||
pub struct RecoilApi {}
|
||||
|
||||
impl RecoilApi {
|
||||
/// Get the value of an atom. Returns a reference to the underlying data.
|
||||
pub fn get<T: PartialEq>(&self, t: &'static Atom<T>) -> Rc<T> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Replace an existing value with a new value
|
||||
///
|
||||
/// This does not replace the value instantly.
|
||||
/// All calls to "get" will return the old value until the component is rendered.
|
||||
pub fn set<T: PartialEq>(&self, t: &'static Atom<T>, new: T) {
|
||||
self.modify(t, move |old| *old = new);
|
||||
}
|
||||
|
||||
/// Modify lets you modify the value in place. However, because there's no previous value around to compare
|
||||
/// the new one with, we are unable to memoize the change. As such, all downsteam users of this Atom will
|
||||
/// be updated, causing all subsrcibed components to re-render.
|
||||
///
|
||||
/// This is fine for most values, but might not be performant when dealing with collections. For collections,
|
||||
/// use the "Family" variants as these will stay memoized for inserts, removals, and modifications.
|
||||
///
|
||||
/// Note - like "set" this won't propogate instantly. Once all "gets" are dropped, only then will the update occur
|
||||
pub fn modify<T: PartialEq, O>(&self, t: &'static Atom<T>, f: impl FnOnce(&mut T) -> O) -> O {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Provide a memoized wrapper around collections for efficient updates.
|
||||
//! --------------------------------------------------------------------
|
||||
//!
|
||||
|
||||
use crate::{AtomValue, FamilyKey, Readable, RecoilItem};
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct atom_family<K: FamilyKey, V: AtomValue>(pub fn(&mut AtomFamilyBuilder<K, V>));
|
||||
pub type AtomFamily<K, V> = atom_family<K, V>;
|
||||
|
||||
// impl<K: FamilyKey, V: AtomValue> Readable for &'static AtomFamily<K, V> {
|
||||
// fn load(&self) -> RecoilItem {
|
||||
// RecoilItem::Atom(*self as *const _ as _)
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct AtomFamilyBuilder<K, V> {
|
||||
_never: std::marker::PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
impl<K: FamilyKey, V: AtomValue> atom_family<K, V> {
|
||||
fn select(&'static self, key: &K) -> FamilySelected {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
struct FamilySelected {}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::{hooks::use_ref, prelude::Context};
|
||||
|
||||
use crate::{Atom, AtomValue, Readable, RecoilApi, RecoilRoot};
|
||||
|
||||
/// This hook initializes the Recoil Context - should only be placed once per app.
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
pub fn init_root(ctx: Context) {
|
||||
ctx.use_create_context(move || RecoilRoot::new())
|
||||
}
|
||||
|
||||
/// Use an atom and its setter
|
||||
///
|
||||
/// This hook subscribes the component to any updates to the atom.
|
||||
///
|
||||
/// ```rust
|
||||
/// const TITLE: Atom<&str> = atom(|_| "default");
|
||||
///
|
||||
/// static App: FC<()> = |ctx, props| {
|
||||
/// let (title, set_title) = recoil::use_state(ctx, &TITLE);
|
||||
/// ctx.render(rsx!{
|
||||
/// div {
|
||||
/// "{title}"
|
||||
/// button {"on", onclick: move |_| set_title("on")}
|
||||
/// button {"off", onclick: move |_| set_title("off")}
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
pub fn use_recoil_state<'a, T: PartialEq + 'static>(
|
||||
ctx: Context<'a>,
|
||||
readable: &'static impl Readable<T>,
|
||||
) -> (&'a T, &'a Rc<dyn Fn(T)>) {
|
||||
struct RecoilStateInner<G: AtomValue> {
|
||||
root: Rc<RecoilRoot>,
|
||||
value: Rc<G>,
|
||||
setter: Rc<dyn Fn(G)>,
|
||||
}
|
||||
|
||||
let root = ctx.use_context::<RecoilRoot>();
|
||||
let (subscriber_id, value) = root.subscribe_consumer(readable, ctx.schedule_update());
|
||||
|
||||
ctx.use_hook(
|
||||
move || RecoilStateInner {
|
||||
value,
|
||||
root: root.clone(),
|
||||
setter: Rc::new(move |new_val| root.update_atom(readable, new_val)),
|
||||
},
|
||||
move |hook| {
|
||||
hook.value = hook.root.load_value(readable);
|
||||
(hook.value.as_ref(), &hook.setter)
|
||||
},
|
||||
// Make sure we unsubscribe
|
||||
// It's not *wrong* for a dead component to receive updates, but it is less performant
|
||||
move |hook| hook.root.drop_consumer(subscriber_id),
|
||||
)
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
/// let (title, set_title) = recoil::use_state()
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
pub fn use_recoil_value<'a, T: PartialEq>(ctx: Context<'a>, t: &'static impl Readable<T>) -> &'a T {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Update an atom's value without
|
||||
///
|
||||
/// Enable the ability to set a value without subscribing the componet
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
/// let (title, set_title) = recoil::use_state()
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
pub fn use_set_state<'a, T: PartialEq>(c: Context<'a>, t: &'static Atom<T>) -> &'a Rc<dyn Fn(T)> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```ignore
|
||||
/// let (title, set_title) = recoil::use_state()
|
||||
///
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
pub fn use_recoil_callback<'a, F: 'a>(
|
||||
ctx: Context<'a>,
|
||||
f: impl Fn(RecoilApi) -> F + 'static,
|
||||
) -> &F {
|
||||
todo!()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mod atom;
|
||||
mod callback;
|
||||
mod family;
|
||||
mod hooks;
|
||||
mod root;
|
||||
mod selector;
|
||||
mod traits;
|
||||
|
||||
pub use atom::*;
|
||||
pub use callback::*;
|
||||
pub use family::*;
|
||||
pub use hooks::*;
|
||||
pub use root::*;
|
||||
pub use selector::*;
|
||||
pub use traits::*;
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicU32, AtomicUsize},
|
||||
};
|
||||
|
||||
use crate::{Atom, AtomBuilder, AtomValue, Readable};
|
||||
|
||||
pub type OpaqueConsumerCallback = Box<dyn Any>;
|
||||
|
||||
struct Consumer {
|
||||
callback: OpaqueConsumerCallback,
|
||||
}
|
||||
|
||||
static SUBSCRIBER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
fn next_id() -> u32 {
|
||||
SUBSCRIBER_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
type AtomId = u32;
|
||||
type ConsumerId = u32;
|
||||
|
||||
pub struct RecoilRoot {
|
||||
consumers: RefCell<HashMap<AtomId, Box<dyn RecoilSlot>>>,
|
||||
}
|
||||
|
||||
impl RecoilRoot {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
consumers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// This is run by hooks when they register as a listener for a given atom
|
||||
// Their ID is stored in the `atom_consumers` map which lets us know which consumers to update
|
||||
// When the hook is dropped, they are unregistered from this map
|
||||
//
|
||||
// If the Atom they're registering as a listener for doesn't exist, then the atom
|
||||
// is initialized. Otherwise, the most recent value of the atom is returned
|
||||
// pub fn register_readable<T: AtomValue>(&self, atom: &impl Readable<T>) -> Rc<T> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
/// This registers the updater fn to any updates to the readable
|
||||
/// Whenever the readable changes, the updater Fn will be called.
|
||||
///
|
||||
/// This also back-propogates changes, meaning components that update an atom
|
||||
/// will be updated from their subscription instead of directly within their own hook.
|
||||
pub fn subscribe_consumer<T: AtomValue>(
|
||||
&self,
|
||||
atom: &'static impl Readable<T>,
|
||||
updater: impl Fn(),
|
||||
// return the value and the consumer's ID
|
||||
// the consumer needs to store its ID so it can properly unsubscribe from the value
|
||||
) -> (u32, Rc<T>) {
|
||||
let id = next_id();
|
||||
|
||||
// Get the raw static reference of the atom
|
||||
let atom_ptr = get_atom_raw_ref(atom);
|
||||
let mut consumers = self.consumers.borrow_mut();
|
||||
if !consumers.contains_key(&atom_ptr) {
|
||||
// Run the atom's initialization
|
||||
// let mut b = AtomBuilder::<T>::new();
|
||||
// let inital_value = atom.0(&mut b);
|
||||
|
||||
consumers.insert(atom_ptr, HashMap::new());
|
||||
}
|
||||
|
||||
todo!()
|
||||
|
||||
// Rc::new(inital_value)
|
||||
// todo!()
|
||||
// // Get the raw static reference of the atom
|
||||
// let atom_ptr = get_atom_raw_ref(atom);
|
||||
|
||||
// // Subcribe this hook_id to this atom
|
||||
// let mut consumers = self.consumers.borrow_mut();
|
||||
// let hooks = consumers
|
||||
// .get_mut(&atom_ptr)
|
||||
// .expect("Atom must be initialized before being subscribed to");
|
||||
|
||||
// // Coerce into any by wrapping the updater in a box
|
||||
// // (yes it's some weird indirection)
|
||||
// let any_updater: OpaqueConsumerCallback = Box::new(updater);
|
||||
// let consumer = Consumer {
|
||||
// callback: any_updater,
|
||||
// };
|
||||
|
||||
// Insert into the map, booting out the old consumer
|
||||
// TODO @Jon, make the "Consumer" more efficient, patching its update mechanism in-place
|
||||
// hooks.insert(hook_id, consumer);
|
||||
}
|
||||
|
||||
pub fn drop_consumer(&self, subscriber_id: u32) {
|
||||
// let mut consumers = self.consumers.borrow_mut();
|
||||
// let atom_ptr = get_atom_raw_ref(atom);
|
||||
// let atoms = consumers.get_mut(&atom_ptr);
|
||||
|
||||
// if let Some(consumers) = atoms {
|
||||
// let entry = consumers.remove_entry(&hook_id);
|
||||
// if let Some(_) = entry {
|
||||
// log::debug!("successfully unsubscribed listener");
|
||||
// } else {
|
||||
// log::debug!("Failure to unsubscribe");
|
||||
// }
|
||||
// } else {
|
||||
// log::debug!("Strange error, atoms should be registed if the consumer is being dropped");
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn update_atom<T: AtomValue>(&self, atom: &impl Readable<T>, new_val: T) {
|
||||
// Get the raw static reference of the atom
|
||||
// let atom_ptr = get_atom_raw_ref(atom);
|
||||
|
||||
// let mut consumers = self.consumers.borrow_mut();
|
||||
// let hooks = consumers
|
||||
// .get_mut(&atom_ptr)
|
||||
// .expect("Atom needs to be registered before trying to update it");
|
||||
|
||||
// let new_val = Rc::new(new_val);
|
||||
// for hook in hooks.values_mut() {
|
||||
// let callback: &mut Rc<ConsumerCallback<T>> = hook
|
||||
// .callback
|
||||
// .downcast_mut::<_>()
|
||||
// .expect("Wrong type of atom stored, internal error");
|
||||
|
||||
// callback(UpdateAction::Regenerate(new_val.clone()));
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn load_value<T: AtomValue>(&self, atom: &impl Readable<T>) -> Rc<T> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
trait RecoilSlot {}
|
||||
|
||||
// struct RecoilSlot {
|
||||
// consumers: HashMap<u32, Box<dyn Fn()>>,
|
||||
// }
|
||||
|
||||
fn get_atom_raw_ref<T: AtomValue>(atom: &'static impl Readable<T>) -> u32 {
|
||||
let atom_ptr = atom as *const _;
|
||||
let atom_ptr = atom_ptr as *const u32;
|
||||
atom_ptr as u32
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::Atom;
|
||||
|
||||
// =====================================
|
||||
// Selectors
|
||||
// =====================================
|
||||
pub struct SelectorApi {}
|
||||
impl SelectorApi {
|
||||
pub fn get<T: PartialEq>(&self, t: &'static Atom<T>) -> &T {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
// pub struct SelectorBuilder<Out, const Built: bool> {
|
||||
// _p: std::marker::PhantomData<Out>,
|
||||
// }
|
||||
// impl<O> SelectorBuilder<O, false> {
|
||||
// pub fn getter(self, f: impl Fn(()) -> O) -> SelectorBuilder<O, true> {
|
||||
// todo!()
|
||||
// // std::rc::Rc::pin(value)
|
||||
// // todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct selector<O>(pub fn(&SelectorApi) -> O);
|
||||
// pub struct selector<O>(pub fn(SelectorBuilder<O, false>) -> SelectorBuilder<O, true>);
|
||||
pub type Selector<O> = selector<O>;
|
||||
@@ -0,0 +1,13 @@
|
||||
use crate::{Atom, AtomFamily};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
pub trait FamilyKey: PartialEq + Hash {}
|
||||
impl<T: PartialEq + Hash> FamilyKey for T {}
|
||||
|
||||
pub trait AtomValue: PartialEq {}
|
||||
impl<T: PartialEq> AtomValue for T {}
|
||||
|
||||
pub trait Readable<T>: 'static {
|
||||
fn load(&'static self) -> RecoilItem;
|
||||
}
|
||||
+125
-139
@@ -1,176 +1,162 @@
|
||||
// ========================
|
||||
// Important hooks
|
||||
// ========================
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
collections::HashMap,
|
||||
hash::Hash,
|
||||
marker::PhantomData,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub fn init_recoil_root(ctx: Context) {
|
||||
let res = ctx.try_use_context::<RecoilRoot>();
|
||||
ctx.create_context(move || {
|
||||
//
|
||||
if res.is_ok() {
|
||||
panic!(
|
||||
"Cannot initialize a recoil root inside of a recoil root.\n An app may only have one recoil root per virtual dom instance"
|
||||
);
|
||||
}
|
||||
RecoilRoot {}
|
||||
})
|
||||
}
|
||||
pub trait FamilyKey: PartialEq + Hash {}
|
||||
impl<T: PartialEq + Hash> FamilyKey for T {}
|
||||
|
||||
pub fn use_atom<'a, T: PartialEq>(c: Context<'a>, t: &'static Atom<T>) -> &'a T {
|
||||
todo!()
|
||||
}
|
||||
pub trait AtomValue: PartialEq + Clone {}
|
||||
impl<T: PartialEq + Clone> AtomValue for T {}
|
||||
|
||||
pub fn use_set_atom<'a, T: PartialEq>(c: Context<'a>, t: &'static Atom<T>) -> Rc<dyn Fn(T)> {
|
||||
todo!()
|
||||
}
|
||||
// Atoms, selectors, and their family variants are readable
|
||||
pub trait Readable<T> {}
|
||||
|
||||
use std::rc::Rc;
|
||||
// Only atoms and atom families are writable
|
||||
// Selectors are not
|
||||
pub trait Writeable<T>: Readable<T> {}
|
||||
|
||||
use dioxus_core::virtual_dom::Context;
|
||||
// =================
|
||||
// Atoms
|
||||
// =================
|
||||
|
||||
pub struct RecoilRoot {}
|
||||
pub struct AtomBuilder {}
|
||||
pub type Atom<T> = fn(&mut AtomBuilder) -> T;
|
||||
impl<T> Readable<T> for Atom<T> {}
|
||||
impl<T> Writeable<T> for Atom<T> {}
|
||||
|
||||
pub struct RecoilContext {}
|
||||
pub type AtomFamily<K, V, F = HashMap<K, V>> = fn((K, V)) -> F;
|
||||
|
||||
impl RecoilContext {
|
||||
/// Get the value of an atom. Returns a reference to the underlying data.
|
||||
pub fn get<T: PartialEq>(&self, t: &'static Atom<T>) -> &T {
|
||||
pub trait SelectionSelector<K, V> {
|
||||
fn select(&self, k: &K) -> CollectionSelection<V> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
impl<K, V, F> SelectionSelector<K, V> for AtomFamily<K, V, F> {}
|
||||
|
||||
/// Replace an existing value with a new value
|
||||
///
|
||||
/// This does not replace the value instantly.
|
||||
/// All calls to "get" will return the old value until the component is rendered.
|
||||
pub fn set<T: PartialEq>(&self, t: &'static Atom<T>, new: T) {
|
||||
self.modify(t, move |old| *old = new);
|
||||
pub trait FamilyCollection<K, V> {}
|
||||
impl<K, V> FamilyCollection<K, V> for HashMap<K, V> {}
|
||||
pub struct CollectionSelection<T> {
|
||||
_never: PhantomData<T>,
|
||||
}
|
||||
impl<T> Readable<T> for CollectionSelection<T> {}
|
||||
|
||||
// =================
|
||||
// Selectors
|
||||
// =================
|
||||
pub struct SelectorBuilder {}
|
||||
impl SelectorBuilder {
|
||||
pub fn get<T: PartialEq>(&self, t: &impl Readable<T>) -> &T {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
pub type Selector<T> = fn(&mut SelectorBuilder) -> T;
|
||||
impl<T> Readable<T> for Selector<T> {}
|
||||
|
||||
/// Modify lets you modify the value in place. However, because there's no previous value around to compare
|
||||
/// the new one with, we are unable to memoize the change. As such, all downsteam users of this Atom will
|
||||
/// be updated, causing all subsrcibed components to re-render.
|
||||
///
|
||||
/// This is fine for most values, but might not be performant when dealing with collections. For collections,
|
||||
/// use the "Family" variants as these will stay memoized for inserts, removals, and modifications.
|
||||
///
|
||||
/// Note - like "set" this won't propogate instantly. Once all "gets" are dropped, only then will the update occur
|
||||
pub struct SelectorFamilyBuilder {}
|
||||
|
||||
impl SelectorFamilyBuilder {
|
||||
pub fn get<T: PartialEq>(&self, t: &impl Readable<T>) -> &T {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> Readable<V> for SelectorFamily<K, V> {}
|
||||
|
||||
/// 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 {}
|
||||
// =================
|
||||
// API
|
||||
// =================
|
||||
pub struct RecoilApi {}
|
||||
impl RecoilApi {
|
||||
pub fn get<T: PartialEq>(&self, t: &'static Atom<T>) -> Rc<T> {
|
||||
todo!()
|
||||
}
|
||||
pub fn modify<T: PartialEq, O>(&self, t: &'static Atom<T>, f: impl FnOnce(&mut T) -> O) -> O {
|
||||
todo!()
|
||||
}
|
||||
pub fn set<T: PartialEq>(&self, t: &'static Atom<T>, new: T) {
|
||||
self.modify(t, move |old| *old = new);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_recoil_context<T>(c: Context) -> &T {
|
||||
todo!()
|
||||
// ================
|
||||
// Root
|
||||
// ================
|
||||
type AtomId = u32;
|
||||
type ConsumerId = u32;
|
||||
|
||||
pub struct RecoilRoot {
|
||||
consumers: RefCell<HashMap<AtomId, Box<dyn RecoilSlot>>>,
|
||||
}
|
||||
|
||||
// pub fn use_callback<'a>(c: &Context<'a>, f: impl Fn() -> G) -> &'a RecoilContext {
|
||||
// todo!()
|
||||
// }
|
||||
trait RecoilSlot {}
|
||||
|
||||
pub trait Readable {}
|
||||
impl<T: PartialEq> Readable for &'static Atom<T> {}
|
||||
impl<K: PartialEq, V: PartialEq> Readable for &'static AtomFamily<K, V> {}
|
||||
|
||||
pub fn use_atom_family<'a, K: PartialEq, V: PartialEq>(
|
||||
c: &Context<'a>,
|
||||
t: &'static AtomFamily<K, V>,
|
||||
g: K,
|
||||
) -> &'a V {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub struct AtomBuilder<T: PartialEq> {
|
||||
pub key: String,
|
||||
pub manual_init: Option<Box<dyn Fn() -> T>>,
|
||||
_never: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq> AtomBuilder<T> {
|
||||
pub fn new() -> Self {
|
||||
impl RecoilRoot {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
key: "".to_string(),
|
||||
manual_init: None,
|
||||
_never: std::marker::PhantomData {},
|
||||
consumers: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<A: Fn() -> T + 'static>(&mut self, f: A) {
|
||||
self.manual_init = Some(Box::new(f));
|
||||
pub use hooks::*;
|
||||
mod hooks {
|
||||
use super::*;
|
||||
use dioxus_core::prelude::Context;
|
||||
|
||||
pub fn use_init_recoil_root(ctx: Context) {
|
||||
ctx.use_create_context(move || RecoilRoot::new())
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, _key: &'static str) {}
|
||||
}
|
||||
pub fn use_set_state<'a, T: PartialEq>(
|
||||
c: Context<'a>,
|
||||
t: &'static impl Writeable<T>,
|
||||
) -> &'a Rc<dyn Fn(T)> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// Atom
|
||||
// =====================================
|
||||
pub struct atom<T: PartialEq>(pub fn(&mut AtomBuilder<T>) -> T);
|
||||
pub type Atom<T: PartialEq> = atom<T>;
|
||||
pub fn use_recoil_state<'a, T: PartialEq + 'static>(
|
||||
ctx: Context<'a>,
|
||||
readable: &'static impl Writeable<T>,
|
||||
) -> (&'a T, &'a Rc<dyn Fn(T)>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// Atom Family
|
||||
// =====================================
|
||||
pub struct AtomFamilyBuilder<K, V> {
|
||||
_never: std::marker::PhantomData<(K, V)>,
|
||||
}
|
||||
pub fn use_recoil_value<'a, T: PartialEq>(
|
||||
ctx: Context<'a>,
|
||||
t: &'static impl Readable<T>,
|
||||
) -> &'a T {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub struct atom_family<K: PartialEq, V: PartialEq>(pub fn(&mut AtomFamilyBuilder<K, V>));
|
||||
pub type AtomFamily<K: PartialEq, V: PartialEq> = atom_family<K, V>;
|
||||
|
||||
// =====================================
|
||||
// Selectors
|
||||
// =====================================
|
||||
pub struct SelectorApi {}
|
||||
impl SelectorApi {
|
||||
pub fn get<T: PartialEq>(&self, t: &'static Atom<T>) -> &T {
|
||||
pub fn use_recoil_callback<'a, F: 'a>(
|
||||
ctx: Context<'a>,
|
||||
f: impl Fn(RecoilApi) -> F + 'static,
|
||||
) -> &F {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
// pub struct SelectorBuilder<Out, const Built: bool> {
|
||||
// _p: std::marker::PhantomData<Out>,
|
||||
// }
|
||||
// impl<O> SelectorBuilder<O, false> {
|
||||
// pub fn getter(self, f: impl Fn(()) -> O) -> SelectorBuilder<O, true> {
|
||||
// todo!()
|
||||
// // std::rc::Rc::pin(value)
|
||||
// // todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct selector<O>(pub fn(&SelectorApi) -> O);
|
||||
// pub struct selector<O>(pub fn(SelectorBuilder<O, false>) -> SelectorBuilder<O, true>);
|
||||
pub type Selector<O> = selector<O>;
|
||||
|
||||
pub fn use_selector<'a, O>(c: Context<'a>, s: &'static Selector<O>) -> &'a O {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn callback_example(ctx: Context) {
|
||||
let set_user = use_recoil_callback(ctx, |api| {
|
||||
|a: String, b: ()| {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
||||
let set_user = use_recoil_callback(ctx, |api| {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
fn use_recoil_callback<F>(ctx: Context, f: impl Fn(RecoilContext) -> F) -> F {
|
||||
todo!()
|
||||
}
|
||||
|
||||
mod analyzer {
|
||||
pub use ecs::*;
|
||||
mod ecs {
|
||||
use super::*;
|
||||
|
||||
const TITLE: Atom<&'static str> = atom(|_| Default::default());
|
||||
|
||||
// pub fn use_add_todo(ctx: Context) -> impl Fn(&'static str) + '_ {
|
||||
// use_recoil_callback(ctx, move |api| move |a: &'static str| api.set(&TITLE, a))
|
||||
// }
|
||||
|
||||
struct Analyzer(RecoilContext);
|
||||
fn use_analyzer(ctx: Context) -> Analyzer {
|
||||
use_recoil_callback(ctx, |api| Analyzer(api))
|
||||
pub struct Blah<K, V> {
|
||||
_p: PhantomData<(K, V)>,
|
||||
}
|
||||
pub type EcsModel<K, Ty> = fn(Blah<K, Ty>);
|
||||
}
|
||||
Reference in new issue
Block a user