Bläddra i källkod

Merge branch 'master' into jk/partialexpansion

Jonathan Kelley 3 år sedan
förälder
incheckning
7c788e59f5

+ 4 - 5
.github/workflows/macos.yml

@@ -1,7 +1,10 @@
 name: macOS tests
 
 on:
-  push:
+  pull_request:
+    types: [opened, synchronize, reopened, ready_for_review]
+    branches:
+      - master
     paths:
       - packages/**
       - examples/**
@@ -9,10 +12,6 @@ on:
       - .github/**
       - lib.rs
       - Cargo.toml
-  pull_request:
-    types: [opened, synchronize, reopened, ready_for_review]
-    branches:
-      - master
 
 jobs:
   test:

+ 24 - 24
.github/workflows/main.yml

@@ -1,7 +1,10 @@
 name: Rust CI
 
 on:
-  push:
+  pull_request:
+    types: [opened, synchronize, reopened, ready_for_review]
+    branches:
+      - master
     paths:
       - packages/**
       - examples/**
@@ -9,10 +12,6 @@ on:
       - .github/**
       - lib.rs
       - Cargo.toml
-  pull_request:
-    types: [opened, synchronize, reopened, ready_for_review]
-    branches:
-      - master
 
 jobs:
   check:
@@ -93,22 +92,23 @@ jobs:
           command: clippy
           args: -- -D warnings
 
-  coverage:
-    if: github.event.pull_request.draft == false
-    name: Coverage
-    runs-on: ubuntu-latest
-    container:
-      image: xd009642/tarpaulin:develop-nightly
-      options: --security-opt seccomp=unconfined
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v2
-      - name: Generate code coverage
-        run: |
-          apt-get update &&\
-          apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\
-          cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml
-      - name: Upload to codecov.io
-        uses: codecov/codecov-action@v2
-        with:
-          fail_ci_if_error: false
+  # Coverage is disabled until we can fix it
+  # coverage:
+  #   name: Coverage
+  #   runs-on: ubuntu-latest
+  #   container:
+  #     image: xd009642/tarpaulin:develop-nightly
+  #     options: --security-opt seccomp=unconfined
+  #   steps:
+  #     - name: Checkout repository
+  #       uses: actions/checkout@v2
+  #     - name: Generate code coverage
+  #       run: |
+  #         apt-get update &&\
+  #         apt-get install build-essential &&\
+  #         apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\
+  #         cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml
+  #     - name: Upload to codecov.io
+  #       uses: codecov/codecov-action@v2
+  #       with:
+  #         fail_ci_if_error: false

+ 4 - 5
.github/workflows/windows.yml

@@ -1,7 +1,10 @@
 name: windows
 
 on:
-  push:
+  pull_request:
+    types: [opened, synchronize, reopened, ready_for_review]
+    branches:
+      - master
     paths:
       - packages/**
       - examples/**
@@ -9,10 +12,6 @@ on:
       - .github/**
       - lib.rs
       - Cargo.toml
-  pull_request:
-    types: [opened, synchronize, reopened, ready_for_review]
-    branches:
-      - master
 
 jobs:
   test:

+ 3 - 15
Cargo.toml

@@ -16,6 +16,7 @@ dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true }
 dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true }
 dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true }
 dioxus-macro-inner = { path = "./packages/rsx", optional = true }
+fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true }
 
 dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true }
 dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true }
@@ -37,20 +38,6 @@ web = ["dioxus-web"]
 desktop = ["dioxus-desktop"]
 router = ["dioxus-router"]
 
-devtool = ["dioxus-desktop/devtool"]
-fullscreen = ["dioxus-desktop/fullscreen"]
-transparent = ["dioxus-desktop/transparent"]
-
-tray = ["dioxus-desktop/tray"]
-ayatana = ["dioxus-desktop/ayatana"]
-
-# "dioxus-router/web"
-# "dioxus-router/desktop"
-# desktop = ["dioxus-desktop", "dioxus-router/desktop"]
-# mobile = ["dioxus-mobile"]
-# liveview = ["dioxus-liveview"]
-
-
 [workspace]
 members = [
     "packages/core",
@@ -63,6 +50,7 @@ members = [
     "packages/desktop",
     "packages/mobile",
     "packages/interpreter",
+    "packages/fermi",
 ]
 
 [dev-dependencies]
@@ -77,4 +65,4 @@ serde_json = "1.0.79"
 rand = { version = "0.8.4", features = ["small_rng"] }
 tokio = { version = "1.16.1", features = ["full"] }
 reqwest = { version = "0.11.9", features = ["json"] }
-dioxus = { path = ".", features = ["desktop", "ssr", "router"] }
+dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] }

+ 1 - 0
docs/fermi/.gitignore

@@ -0,0 +1 @@
+book

+ 6 - 0
docs/fermi/book.toml

@@ -0,0 +1,6 @@
+[book]
+authors = ["Jonathan Kelley"]
+language = "en"
+multilingual = false
+src = "src"
+title = "Fermi Guide"

+ 3 - 0
docs/fermi/src/SUMMARY.md

@@ -0,0 +1,3 @@
+# Summary
+
+- [Chapter 1](./chapter_1.md)

+ 1 - 0
docs/fermi/src/chapter_1.md

@@ -0,0 +1 @@
+# Chapter 1

+ 12 - 12
docs/reference/src/platforms/ssr.md

@@ -2,11 +2,8 @@
 
 The Dioxus VirtualDom can be rendered to a string by traversing the Element Tree. This is implemented in the `ssr` crate where your Dioxus app can be directly rendered to HTML to be served by a web server.
 
-
-
 ## Setup
 
-
 If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple:
 
 ```rust
@@ -19,8 +16,7 @@ println!("{}", dioxus::ssr::render_vdom(&vdom));
 println!( "{}", dioxus::ssr::render_lazy(rsx! { h1 { "Hello, world!" } } );
 ```
 
-
-However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. 
+However, for this guide, we're going to show how to use Dioxus SSR with `Axum`.
 
 Make sure you have Rust and Cargo installed, and then create a new project:
 
@@ -29,15 +25,16 @@ $ cargo new --bin demo
 $ cd app
 ```
 
-Add Dioxus with the `desktop` feature:
+Add Dioxus with the `ssr` feature:
 
 ```shell
 $ cargo add dioxus --features ssr
 ```
 
 Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
+
 ```
-$ cargo add dioxus tokio --features full
+$ cargo add tokio --features full
 $ cargo add axum
 ```
 
@@ -45,12 +42,11 @@ Your dependencies should look roughly like this:
 
 ```toml
 [dependencies]
-axum = "0.4.3"
+axum = "0.4.5"
 dioxus = { version = "*", features = ["ssr"] }
 tokio = { version = "1.15.0", features = ["full"] }
 ```
 
-
 Now, setup your Axum app to respond on an endpoint.
 
 ```rust
@@ -63,7 +59,11 @@ async fn main() {
     println!("listening on http://{}", addr);
 
     axum::Server::bind(&addr)
-        .serve(Router::new().route("/", get(app_endpoint)))
+        .serve(
+            Router::new()
+                .route("/", get(app_endpoint))
+                .into_make_service(),
+        )
         .await
         .unwrap();
 }
@@ -88,14 +88,14 @@ async fn app_endpoint() -> Html<String> {
     }
     let mut app = VirtualDom::new(app);
     let _ = app.rebuild();
-    
+
     Html(dioxus::ssr::render_vdom(&app))
 }
 ```
 
 And that's it!
 
-> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it *must* remain on the thread it started. We are working on loosening this requirement.
+> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement.
 
 ## Future Steps
 

+ 30 - 0
examples/fermi.rs

@@ -0,0 +1,30 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use fermi::prelude::*;
+
+fn main() {
+    dioxus::desktop::launch(app)
+}
+
+static NAME: Atom<String> = |_| "world".to_string();
+
+fn app(cx: Scope) -> Element {
+    let name = use_read(&cx, NAME);
+
+    cx.render(rsx! {
+        div { "hello {name}!" }
+        Child {}
+    })
+}
+
+fn Child(cx: Scope) -> Element {
+    let set_name = use_set(&cx, NAME);
+
+    cx.render(rsx! {
+        button {
+            onclick: move |_| set_name("dioxus".to_string()),
+            "reset name"
+        }
+    })
+}

+ 14 - 0
packages/fermi/Cargo.toml

@@ -0,0 +1,14 @@
+[package]
+name = "fermi"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dioxus-core = { path = "../core" }
+im-rc = { version = "15.0.0", features = ["serde"] }
+log = "0.4.14"
+
+[dev-dependencies]
+closure = "0.3.0"

+ 92 - 0
packages/fermi/README.md

@@ -0,0 +1,92 @@
+
+<div align="center">
+  <h1>Fermi ⚛</h1>
+  <p>
+    <strong>Atom-based global state management solution for Dioxus</strong>
+  </p>
+</div>
+
+
+<div align="center">
+  <!-- Crates version -->
+  <a href="https://crates.io/crates/dioxus">
+    <img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
+    alt="Crates.io version" />
+  </a>
+  <!-- Downloads -->
+  <a href="https://crates.io/crates/dioxus">
+    <img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
+      alt="Download" />
+  </a>
+  <!-- docs -->
+  <a href="https://docs.rs/dioxus">
+    <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
+      alt="docs.rs docs" />
+  </a>
+  <!-- CI -->
+  <a href="https://github.com/jkelleyrtp/dioxus/actions">
+    <img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
+      alt="CI status" />
+  </a>
+</div>
+
+-----
+
+Fermi is a global state management solution for Dioxus that's as easy as `use_state`.
+
+Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`:
+
+```rust
+static NAME: Atom<&str> = |_| "Dioxus";
+```
+
+From anywhere in our app, we can read our the value of our atom:
+
+```rust
+fn NameCard(cx: Scope) -> Element {      
+    let name = use_read(&cx, NAME);
+    cx.render(rsx!{ h1 { "Hello, {name}"} })
+}
+```
+
+We can also set the value of our atom, also from anywhere in our app:
+
+```rust
+fn NameCard(cx: Scope) -> Element {      
+    let set_name = use_set(&cx, NAME);
+    cx.render(rsx!{
+        button {
+            onclick: move |_| set_name("Fermi"),
+            "Set name to fermi"
+        }
+    })
+}
+```
+
+It's that simple!
+
+## Installation
+Fermi is currently under construction, so you have to use the `master` branch to get started.
+
+```rust
+[depdencies]
+fermi = { git = "https://github.com/dioxuslabs/fermi" }
+```
+
+
+## Running examples
+
+The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
+```
+$ cargo run --example EXAMPLE
+```
+
+## Features
+
+Broadly our feature set to required to be released includes:
+- [x] Support for Atoms
+- [x] Support for AtomRef (for values that aren't clone)
+- [ ] Support for Atom Families
+- [ ] Support for memoized Selectors
+- [ ] Support for memoized SelectorFamilies
+- [ ] Support for UseFermiCallback for access to fermi from async 

+ 28 - 0
packages/fermi/src/atoms/atom.rs

@@ -0,0 +1,28 @@
+use crate::{AtomId, AtomRoot, Readable, Writable};
+
+pub type Atom<T> = fn(AtomBuilder) -> T;
+pub struct AtomBuilder;
+
+impl<V> Readable<V> for Atom<V> {
+    fn read(&self, _root: AtomRoot) -> Option<V> {
+        todo!()
+    }
+    fn init(&self) -> V {
+        (*self)(AtomBuilder)
+    }
+    fn unique_id(&self) -> AtomId {
+        *self as *const ()
+    }
+}
+
+impl<V> Writable<V> for Atom<V> {
+    fn write(&self, _root: AtomRoot, _value: V) {
+        todo!()
+    }
+}
+
+#[test]
+fn atom_compiles() {
+    static TEST_ATOM: Atom<&str> = |_| "hello";
+    dbg!(TEST_ATOM.init());
+}

+ 25 - 0
packages/fermi/src/atoms/atomfamily.rs

@@ -0,0 +1,25 @@
+use crate::{AtomId, AtomRoot, Readable, Writable};
+use im_rc::HashMap as ImMap;
+
+pub struct AtomFamilyBuilder;
+pub type AtomFamily<K, V> = fn(AtomFamilyBuilder) -> ImMap<K, V>;
+
+impl<K, V> Readable<ImMap<K, V>> for AtomFamily<K, V> {
+    fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> {
+        todo!()
+    }
+
+    fn init(&self) -> ImMap<K, V> {
+        (*self)(AtomFamilyBuilder)
+    }
+
+    fn unique_id(&self) -> AtomId {
+        *self as *const ()
+    }
+}
+
+impl<K, V> Writable<ImMap<K, V>> for AtomFamily<K, V> {
+    fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) {
+        todo!()
+    }
+}

+ 25 - 0
packages/fermi/src/atoms/atomref.rs

@@ -0,0 +1,25 @@
+use crate::{AtomId, AtomRoot, Readable};
+use std::cell::RefCell;
+
+pub struct AtomRefBuilder;
+pub type AtomRef<T> = fn(AtomRefBuilder) -> T;
+
+impl<V> Readable<RefCell<V>> for AtomRef<V> {
+    fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> {
+        todo!()
+    }
+
+    fn init(&self) -> RefCell<V> {
+        RefCell::new((*self)(AtomRefBuilder))
+    }
+
+    fn unique_id(&self) -> AtomId {
+        *self as *const ()
+    }
+}
+
+#[test]
+fn atom_compiles() {
+    static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![];
+    dbg!(TEST_ATOM.init());
+}

+ 1 - 0
packages/fermi/src/atoms/selector.rs

@@ -0,0 +1 @@
+

+ 1 - 0
packages/fermi/src/atoms/selectorfamily.rs

@@ -0,0 +1 @@
+

+ 53 - 0
packages/fermi/src/callback.rs

@@ -0,0 +1,53 @@
+#![allow(clippy::all, unused)]
+
+use std::rc::Rc;
+
+use dioxus_core::prelude::*;
+
+use crate::{AtomRoot, Readable, Writable};
+
+#[derive(Clone)]
+pub struct CallbackApi {
+    root: Rc<AtomRoot>,
+}
+
+impl CallbackApi {
+    // get the current value of the atom
+    pub fn get<V>(&self, atom: impl Readable<V>) -> &V {
+        todo!()
+    }
+
+    // get the current value of the atom in its RC container
+    pub fn get_rc<V>(&self, atom: impl Readable<V>) -> &Rc<V> {
+        todo!()
+    }
+
+    // set the current value of the atom
+    pub fn set<V>(&self, atom: impl Writable<V>, value: V) {
+        todo!()
+    }
+}
+
+pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
+    todo!()
+}
+
+macro_rules! use_callback {
+    (&$cx:ident, [$($cap:ident),*],  move || $body:expr) => {
+        move || {
+            $(
+                #[allow(unused_mut)]
+                let mut $cap = $cap.to_owned();
+            )*
+            $cx.spawn($body);
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! to_owned {
+    ($($es:ident),+) => {$(
+        #[allow(unused_mut)]
+        let mut $es = $es.to_owned();
+    )*}
+}

+ 62 - 0
packages/fermi/src/hooks/atom_ref.rs

@@ -0,0 +1,62 @@
+use crate::{use_atom_root, AtomId, AtomRef, AtomRoot, Readable};
+use dioxus_core::{ScopeId, ScopeState};
+use std::{
+    cell::{Ref, RefCell, RefMut},
+    rc::Rc,
+};
+
+///
+///
+///
+///
+///
+///
+///
+///
+pub fn use_atom_ref<T: 'static>(cx: &ScopeState, atom: AtomRef<T>) -> &UseAtomRef<T> {
+    let root = use_atom_root(cx);
+
+    cx.use_hook(|_| {
+        root.initialize(atom);
+        UseAtomRef {
+            ptr: atom.unique_id(),
+            root: root.clone(),
+            scope_id: cx.scope_id(),
+            value: root.register(atom, cx.scope_id()),
+        }
+    })
+}
+
+pub struct UseAtomRef<T> {
+    ptr: AtomId,
+    value: Rc<RefCell<T>>,
+    root: Rc<AtomRoot>,
+    scope_id: ScopeId,
+}
+
+impl<T: 'static> UseAtomRef<T> {
+    pub fn read(&self) -> Ref<T> {
+        self.value.borrow()
+    }
+
+    pub fn write(&self) -> RefMut<T> {
+        self.root.force_update(self.ptr);
+        self.value.borrow_mut()
+    }
+
+    pub fn write_silent(&self) -> RefMut<T> {
+        self.root.force_update(self.ptr);
+        self.value.borrow_mut()
+    }
+
+    pub fn set(&self, new: T) {
+        self.root.force_update(self.ptr);
+        self.root.set(self.ptr, new);
+    }
+}
+
+impl<T> Drop for UseAtomRef<T> {
+    fn drop(&mut self) {
+        self.root.unsubscribe(self.ptr, self.scope_id)
+    }
+}

+ 11 - 0
packages/fermi/src/hooks/atom_root.rs

@@ -0,0 +1,11 @@
+use crate::AtomRoot;
+use dioxus_core::ScopeState;
+use std::rc::Rc;
+
+// Returns the atom root, initiaizing it at the root of the app if it does not exist.
+pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
+    cx.use_hook(|_| match cx.consume_context::<AtomRoot>() {
+        Some(root) => root,
+        None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())),
+    })
+}

+ 11 - 0
packages/fermi/src/hooks/init_atom_root.rs

@@ -0,0 +1,11 @@
+use crate::AtomRoot;
+use dioxus_core::ScopeState;
+use std::rc::Rc;
+
+// Initializes the atom root and retuns it;
+pub fn use_init_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
+    cx.use_hook(|_| match cx.consume_context::<AtomRoot>() {
+        Some(ctx) => ctx,
+        None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())),
+    })
+}

+ 36 - 0
packages/fermi/src/hooks/read.rs

@@ -0,0 +1,36 @@
+use crate::{use_atom_root, AtomId, AtomRoot, Readable};
+use dioxus_core::{ScopeId, ScopeState};
+use std::rc::Rc;
+
+pub fn use_read<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable<V>) -> &'a V {
+    use_read_rc(cx, f).as_ref()
+}
+
+pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable<V>) -> &'a Rc<V> {
+    let root = use_atom_root(cx);
+
+    struct UseReadInner<V> {
+        root: Rc<AtomRoot>,
+        id: AtomId,
+        scope_id: ScopeId,
+        value: Option<Rc<V>>,
+    }
+
+    impl<V> Drop for UseReadInner<V> {
+        fn drop(&mut self) {
+            self.root.unsubscribe(self.id, self.scope_id)
+        }
+    }
+
+    let inner = cx.use_hook(|_| UseReadInner {
+        value: None,
+        root: root.clone(),
+        scope_id: cx.scope_id(),
+        id: f.unique_id(),
+    });
+
+    let value = inner.root.register(f, cx.scope_id());
+
+    inner.value = Some(value);
+    inner.value.as_ref().unwrap()
+}

+ 13 - 0
packages/fermi/src/hooks/set.rs

@@ -0,0 +1,13 @@
+use crate::{use_atom_root, Writable};
+use dioxus_core::ScopeState;
+use std::rc::Rc;
+
+pub fn use_set<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable<T>) -> &'a Rc<dyn Fn(T)> {
+    let root = use_atom_root(cx);
+    cx.use_hook(|_| {
+        let id = f.unique_id();
+        let root = root.clone();
+        root.initialize(f);
+        Rc::new(move |new| root.set(id, new)) as Rc<dyn Fn(T)>
+    })
+}

+ 58 - 0
packages/fermi/src/lib.rs

@@ -0,0 +1,58 @@
+#![doc = include_str!("../README.md")]
+
+pub mod prelude {
+    pub use crate::*;
+}
+
+mod callback;
+mod root;
+
+pub use atoms::*;
+pub use callback::*;
+pub use hooks::*;
+pub use root::*;
+
+mod atoms {
+    mod atom;
+    mod atomfamily;
+    mod atomref;
+    mod selector;
+    mod selectorfamily;
+
+    pub use atom::*;
+    pub use atomfamily::*;
+    pub use atomref::*;
+    pub use selector::*;
+    pub use selectorfamily::*;
+}
+
+pub mod hooks {
+    mod atom_ref;
+    mod atom_root;
+    mod init_atom_root;
+    mod read;
+    mod set;
+    pub use atom_ref::*;
+    pub use atom_root::*;
+    pub use init_atom_root::*;
+    pub use read::*;
+    pub use set::*;
+}
+
+/// All Atoms are `Readable` - they support reading their value.
+///
+/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors.
+/// It is not very useful for your own code, but could be used to build new Atom primitives.
+pub trait Readable<V> {
+    fn read(&self, root: AtomRoot) -> Option<V>;
+    fn init(&self) -> V;
+    fn unique_id(&self) -> AtomId;
+}
+
+/// All Atoms are `Writable` - they support writing their value.
+///
+/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors.
+/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors
+pub trait Writable<V>: Readable<V> {
+    fn write(&self, root: AtomRoot, value: V);
+}

+ 103 - 0
packages/fermi/src/root.rs

@@ -0,0 +1,103 @@
+use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc};
+
+use dioxus_core::ScopeId;
+use im_rc::HashSet;
+
+use crate::Readable;
+
+pub type AtomId = *const ();
+
+pub struct AtomRoot {
+    pub atoms: RefCell<HashMap<AtomId, Slot>>,
+    pub update_any: Rc<dyn Fn(ScopeId)>,
+}
+
+pub struct Slot {
+    pub value: Rc<dyn Any>,
+    pub subscribers: HashSet<ScopeId>,
+}
+
+impl AtomRoot {
+    pub fn new(update_any: Rc<dyn Fn(ScopeId)>) -> Self {
+        Self {
+            update_any,
+            atoms: RefCell::new(HashMap::new()),
+        }
+    }
+
+    pub fn initialize<V: 'static>(&self, f: impl Readable<V>) {
+        let id = f.unique_id();
+        if self.atoms.borrow().get(&id).is_none() {
+            self.atoms.borrow_mut().insert(
+                id,
+                Slot {
+                    value: Rc::new(f.init()),
+                    subscribers: HashSet::new(),
+                },
+            );
+        }
+    }
+
+    pub fn register<V: 'static>(&self, f: impl Readable<V>, scope: ScopeId) -> Rc<V> {
+        log::trace!("registering atom {:?}", f.unique_id());
+
+        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.subscribers.insert(scope);
+            slot.value.clone().downcast().unwrap()
+        } else {
+            let value = Rc::new(f.init());
+            let mut subscribers = HashSet::new();
+            subscribers.insert(scope);
+
+            atoms.insert(
+                f.unique_id(),
+                Slot {
+                    value: value.clone(),
+                    subscribers,
+                },
+            );
+            value
+        }
+    }
+
+    pub fn set<V: 'static>(&self, ptr: AtomId, value: V) {
+        let mut atoms = self.atoms.borrow_mut();
+
+        if let Some(slot) = atoms.get_mut(&ptr) {
+            slot.value = Rc::new(value);
+            log::trace!("found item with subscribers {:?}", slot.subscribers);
+
+            for scope in &slot.subscribers {
+                log::trace!("updating subcsriber");
+                (self.update_any)(*scope);
+            }
+        } else {
+            log::trace!("no atoms found for {:?}", ptr);
+        }
+    }
+
+    pub fn unsubscribe(&self, ptr: AtomId, scope: ScopeId) {
+        let mut atoms = self.atoms.borrow_mut();
+
+        if let Some(slot) = atoms.get_mut(&ptr) {
+            slot.subscribers.remove(&scope);
+        }
+    }
+
+    // force update of all subscribers
+    pub fn force_update(&self, ptr: AtomId) {
+        if let Some(slot) = self.atoms.borrow_mut().get(&ptr) {
+            for scope in slot.subscribers.iter() {
+                log::trace!("updating subcsriber");
+                (self.update_any)(*scope);
+            }
+        }
+    }
+
+    pub fn read<V>(&self, _f: impl Readable<V>) -> &V {
+        todo!()
+    }
+}

+ 3 - 0
src/lib.rs

@@ -340,6 +340,9 @@ pub use dioxus_web as web;
 #[cfg(feature = "desktop")]
 pub use dioxus_desktop as desktop;
 
+#[cfg(feature = "fermi")]
+pub use fermi;
+
 // #[cfg(feature = "mobile")]
 // pub use dioxus_mobile as mobile;
 

+ 46 - 0
tests/fermi.rs

@@ -0,0 +1,46 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use fermi::*;
+
+#[test]
+fn test_fermi() {
+    let mut app = VirtualDom::new(App);
+    app.rebuild();
+}
+
+static TITLE: Atom<String> = |_| "".to_string();
+static USERS: AtomFamily<u32, String> = |_| Default::default();
+
+fn App(cx: Scope) -> Element {
+    cx.render(rsx!(
+        Leaf { id: 0 }
+        Leaf { id: 1 }
+        Leaf { id: 2 }
+    ))
+}
+
+#[derive(Debug, PartialEq, Props)]
+struct LeafProps {
+    id: u32,
+}
+
+fn Leaf(cx: Scope<LeafProps>) -> Element {
+    let _user = use_read(&cx, TITLE);
+    let _user = use_read(&cx, USERS);
+
+    rsx!(cx, div {
+        button {
+            onclick: move |_| {},
+            "Start"
+        }
+        button {
+            onclick: move |_| {},
+            "Stop"
+        }
+        button {
+            onclick: move |_| {},
+            "Reverse"
+        }
+    })
+}