").unwrap();
- }
-
- #[test]
- fn nested() {
- parse("bump,
").unwrap();
- }
-
- #[test]
- fn complex() {
- parse(
- "bump,
-
- ",
- )
- .unwrap();
- }
-}
diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs
index 7099311ce..931933386 100644
--- a/packages/core-macro/src/lib.rs
+++ b/packages/core-macro/src/lib.rs
@@ -2,11 +2,10 @@ use proc_macro::TokenStream;
use quote::ToTokens;
use syn::parse_macro_input;
-pub(crate) mod ifmt;
-pub(crate) mod inlineprops;
-pub(crate) mod props;
-pub(crate) mod router;
-pub(crate) mod rsx;
+mod ifmt;
+mod inlineprops;
+mod props;
+mod rsx;
#[proc_macro]
pub fn format_args_f(input: TokenStream) -> TokenStream {
@@ -186,36 +185,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
}
}
-/// Derive macro used to mark an enum as Routable.
-///
-/// This macro can only be used on enums. Every varient of the macro needs to be marked
-/// with the `at` attribute to specify the URL of the route. It generates an implementation of
-/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route`
-/// component.
-///
-/// # Example
-///
-/// ```
-/// # use yew_router::Routable;
-/// #[derive(Debug, Clone, Copy, PartialEq, Routable)]
-/// enum Routes {
-/// #[at("/")]
-/// Home,
-/// #[at("/secure")]
-/// Secure,
-/// #[at("/profile/{id}")]
-/// Profile(u32),
-/// #[at("/404")]
-/// NotFound,
-/// }
-/// ```
-#[proc_macro_derive(Routable, attributes(at, not_found))]
-pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
- use router::{routable_derive_impl, Routable};
- let input = parse_macro_input!(input as Routable);
- routable_derive_impl(input).into()
-}
-
/// Derive props for a component within the component definition.
///
/// This macro provides a simple transformation from `Scope<{}>` to `Scope
`,
diff --git a/packages/core-macro/src/router.rs b/packages/core-macro/src/router.rs
deleted file mode 100644
index 3edad75e3..000000000
--- a/packages/core-macro/src/router.rs
+++ /dev/null
@@ -1,214 +0,0 @@
-#![allow(dead_code)]
-
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::parse::{Parse, ParseStream};
-use syn::punctuated::Punctuated;
-use syn::spanned::Spanned;
-use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
-
-const AT_ATTR_IDENT: &str = "at";
-const NOT_FOUND_ATTR_IDENT: &str = "not_found";
-
-pub struct Routable {
- ident: Ident,
- ats: Vec,
- variants: Punctuated,
- not_found_route: Option,
-}
-
-impl Parse for Routable {
- fn parse(input: ParseStream) -> syn::Result {
- let DeriveInput { ident, data, .. } = input.parse()?;
-
- let data = match data {
- Data::Enum(data) => data,
- Data::Struct(s) => {
- return Err(syn::Error::new(
- s.struct_token.span(),
- "expected enum, found struct",
- ))
- }
- Data::Union(u) => {
- return Err(syn::Error::new(
- u.union_token.span(),
- "expected enum, found union",
- ))
- }
- };
-
- let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
-
- Ok(Self {
- ident,
- variants: data.variants,
- ats,
- not_found_route,
- })
- }
-}
-
-fn parse_variants_attributes(
- variants: &Punctuated,
-) -> syn::Result<(Option, Vec)> {
- let mut not_founds = vec![];
- let mut ats: Vec = vec![];
-
- let mut not_found_attrs = vec![];
-
- for variant in variants.iter() {
- if let Fields::Unnamed(ref field) = variant.fields {
- return Err(syn::Error::new(
- field.span(),
- "only named fields are supported",
- ));
- }
-
- let attrs = &variant.attrs;
- let at_attrs = attrs
- .iter()
- .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT))
- .collect::>();
-
- let attr = match at_attrs.len() {
- 1 => *at_attrs.first().unwrap(),
- 0 => {
- return Err(syn::Error::new(
- variant.span(),
- format!(
- "{} attribute must be present on every variant",
- AT_ATTR_IDENT
- ),
- ))
- }
- _ => {
- return Err(syn::Error::new_spanned(
- quote! { #(#at_attrs)* },
- format!("only one {} attribute must be present", AT_ATTR_IDENT),
- ))
- }
- };
-
- let lit = attr.parse_args::()?;
- ats.push(lit);
-
- for attr in attrs.iter() {
- if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) {
- not_found_attrs.push(attr);
- not_founds.push(variant.ident.clone())
- }
- }
- }
-
- if not_founds.len() > 1 {
- return Err(syn::Error::new_spanned(
- quote! { #(#not_found_attrs)* },
- format!("there can only be one {}", NOT_FOUND_ATTR_IDENT),
- ));
- }
-
- Ok((not_founds.into_iter().next(), ats))
-}
-
-impl Routable {
- // fn build_from_path(&self) -> TokenStream {
- // let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
- // let ident = &variant.ident;
- // let right = match &variant.fields {
- // Fields::Unit => quote! { Self::#ident },
- // Fields::Named(field) => {
- // let fields = field.named.iter().map(|it| {
- // //named fields have idents
- // it.ident.as_ref().unwrap()
- // });
- // quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
- // }
- // Fields::Unnamed(_) => unreachable!(), // already checked
- // };
-
- // let left = self.ats.get(i).unwrap();
- // quote! {
- // #left => ::std::option::Option::Some(#right)
- // }
- // });
-
- // quote! {
- // fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option {
- // match path {
- // #(#from_path_matches),*,
- // _ => ::std::option::Option::None,
- // }
- // }
- // }
- // }
-
- // fn build_to_path(&self) -> TokenStream {
- // let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
- // let ident = &variant.ident;
- // let mut right = self.ats.get(i).unwrap().value();
-
- // match &variant.fields {
- // Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
- // Fields::Named(field) => {
- // let fields = field
- // .named
- // .iter()
- // .map(|it| it.ident.as_ref().unwrap())
- // .collect::>();
-
- // for field in fields.iter() {
- // // :param -> {param}
- // // so we can pass it to `format!("...", param)`
- // right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
- // }
-
- // quote! {
- // Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
- // }
- // }
- // Fields::Unnamed(_) => unreachable!(), // already checked
- // }
- // });
-
- // quote! {
- // fn to_path(&self) -> ::std::string::String {
- // match self {
- // #(#to_path_matches),*,
- // }
- // }
- // }
- // }
-}
-
-pub fn routable_derive_impl(input: Routable) -> TokenStream {
- let Routable {
- // ats,
- // not_found_route,
- // ident,
- ..
- } = &input;
-
- // let from_path = input.build_from_path();
- // let to_path = input.build_to_path();
-
- quote! {
- // #[automatically_derived]
- // impl ::dioxus::router::Routable for #ident {
-
- // fn recognize(pathname: &str) -> ::std::option::Option {
- // todo!()
- // // ::std::thread_local! {
- // // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
- // // }
- // // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
- // // {
- // // let route = ::std::clone::Clone::clone(&route);
- // // #cache_thread_local_ident.with(move |val| {
- // // *val.borrow_mut() = route;
- // // });
- // // }
- // // route
- // }
- // }
- }
-}
diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs
index 4e9d449cf..17006e6ac 100644
--- a/packages/core-macro/src/rsx/component.rs
+++ b/packages/core-macro/src/rsx/component.rs
@@ -23,10 +23,10 @@ use syn::{
};
pub struct Component {
- name: syn::Path,
- body: Vec,
- children: Vec,
- manual_props: Option,
+ pub name: syn::Path,
+ pub body: Vec,
+ pub children: Vec,
+ pub manual_props: Option,
}
impl Parse for Component {
diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs
index ebc401b6e..fa8fc9f51 100644
--- a/packages/core-macro/src/rsx/element.rs
+++ b/packages/core-macro/src/rsx/element.rs
@@ -11,12 +11,11 @@ use syn::{
// Parse the VNode::Element type
// =======================================
pub struct Element {
- name: Ident,
- key: Option,
- attributes: Vec,
- listeners: Vec,
- children: Vec,
- _is_static: bool,
+ pub name: Ident,
+ pub key: Option,
+ pub attributes: Vec,
+ pub children: Vec,
+ pub _is_static: bool,
}
impl Parse for Element {
@@ -28,7 +27,6 @@ impl Parse for Element {
syn::braced!(content in stream);
let mut attributes: Vec = vec![];
- let mut listeners: Vec = vec![];
let mut children: Vec = vec![];
let mut key = None;
let mut _el_ref = None;
@@ -54,6 +52,7 @@ impl Parse for Element {
});
} else {
let value = content.parse::()?;
+
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrExpression { name, value },
@@ -82,7 +81,7 @@ impl Parse for Element {
content.parse::()?;
if name_str.starts_with("on") {
- listeners.push(ElementAttrNamed {
+ attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::EventTokens {
name,
@@ -182,7 +181,6 @@ impl Parse for Element {
name: el_name,
attributes,
children,
- listeners,
_is_static: false,
})
}
@@ -193,14 +191,21 @@ impl ToTokens for Element {
let name = &self.name;
let children = &self.children;
- let listeners = &self.listeners;
- let attr = &self.attributes;
-
let key = match &self.key {
Some(ty) => quote! { Some(format_args_f!(#ty)) },
None => quote! { None },
};
+ let listeners = self
+ .attributes
+ .iter()
+ .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. }));
+
+ let attr = self
+ .attributes
+ .iter()
+ .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. }));
+
tokens.append_all(quote! {
__cx.element(
dioxus_elements::#name,
@@ -213,29 +218,28 @@ impl ToTokens for Element {
}
}
-enum ElementAttr {
- // attribute: "valuee {}"
+pub enum ElementAttr {
+ /// attribute: "valuee {}"
AttrText { name: Ident, value: LitStr },
- // attribute: true,
+ /// attribute: true,
AttrExpression { name: Ident, value: Expr },
- // "attribute": "value {}"
+ /// "attribute": "value {}"
CustomAttrText { name: LitStr, value: LitStr },
- // "attribute": true,
+ /// "attribute": true,
CustomAttrExpression { name: LitStr, value: Expr },
- // // onclick: move |_| {}
+ // /// onclick: move |_| {}
// EventClosure { name: Ident, closure: ExprClosure },
-
- // onclick: {}
+ /// onclick: {}
EventTokens { name: Ident, tokens: Expr },
}
-struct ElementAttrNamed {
- el_name: Ident,
- attr: ElementAttr,
+pub struct ElementAttrNamed {
+ pub el_name: Ident,
+ pub attr: ElementAttr,
}
impl ToTokens for ElementAttrNamed {
diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs
index 5ff5d85f7..858f07c5c 100644
--- a/packages/core-macro/src/rsx/mod.rs
+++ b/packages/core-macro/src/rsx/mod.rs
@@ -15,6 +15,8 @@ mod component;
mod element;
mod node;
+pub mod pretty;
+
// Re-export the namespaces into each other
pub use component::*;
pub use element::*;
@@ -29,8 +31,8 @@ use syn::{
};
pub struct CallBody {
- custom_context: Option,
- roots: Vec,
+ pub custom_context: Option,
+ pub roots: Vec,
}
impl Parse for CallBody {
@@ -77,7 +79,7 @@ impl ToTokens for CallBody {
match &self.custom_context {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
- #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
+ #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
}))
@@ -85,7 +87,7 @@ impl ToTokens for CallBody {
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
- LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
+ LazyNodes::new(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
})
diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs
new file mode 100644
index 000000000..26ad54006
--- /dev/null
+++ b/packages/core-macro/src/rsx/pretty.rs
@@ -0,0 +1 @@
+//! pretty printer for rsx!
diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/core-macro/src/rsxtemplate.rs
deleted file mode 100644
index 9222d7117..000000000
--- a/packages/core-macro/src/rsxtemplate.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-use crate::{rsx::RsxBody, util::is_valid_svg_tag};
-
-use {
- proc_macro::TokenStream,
- proc_macro2::{Span, TokenStream as TokenStream2},
- quote::{quote, ToTokens, TokenStreamExt},
- syn::{
- ext::IdentExt,
- parse::{Parse, ParseStream},
- token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token,
- },
-};
-
-// ==============================================
-// Parse any stream coming from the html! macro
-// ==============================================
-pub struct RsxTemplate {
- inner: RsxBody,
-}
-
-impl Parse for RsxTemplate {
- fn parse(s: ParseStream) -> Result {
- if s.peek(LitStr) {
- use std::str::FromStr;
-
- let lit = s.parse::()?;
- let g = lit.span();
- let mut value = lit.value();
- if value.ends_with('\n') {
- value.pop();
- if value.ends_with('\r') {
- value.pop();
- }
- }
- let lit = LitStr::new(&value, lit.span());
-
- // panic!("{:#?}", lit);
- match lit.parse::() {
- Ok(r) => Ok(Self { inner: r }),
- Err(e) => Err(e),
- }
- } else {
- panic!("Not a str lit")
- }
- // let t = s.parse::()?;
-
- // let new_stream = TokenStream::from(t.to_s)
-
- // let cx: Ident = s.parse()?;
- // s.parse::()?;
- // if elements are in an array, return a bumpalo::collections::Vec rather than a Node.
- // let kind = if s.peek(token::Bracket) {
- // let nodes_toks;
- // syn::bracketed!(nodes_toks in s);
- // let mut nodes: Vec> = vec![nodes_toks.parse()?];
- // while nodes_toks.peek(Token![,]) {
- // nodes_toks.parse::()?;
- // nodes.push(nodes_toks.parse()?);
- // }
- // NodeOrList::List(NodeList(nodes))
- // } else {
- // NodeOrList::Node(s.parse()?)
- // };
- // Ok(HtmlRender { kind })
- }
-}
-
-impl ToTokens for RsxTemplate {
- fn to_tokens(&self, out_tokens: &mut TokenStream2) {
- self.inner.to_tokens(out_tokens);
- // let new_toks = ToToksCtx::new(&self.kind).to_token_stream();
-
- // // create a lazy tree that accepts a bump allocator
- // let final_tokens = quote! {
- // dioxus::prelude::LazyNodes::new(move |cx| {
- // let bump = &cx.bump();
-
- // #new_toks
- // })
- // };
-
- // final_tokens.to_tokens(out_tokens);
- }
-}
diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml
index 03b378e88..4126eb47f 100644
--- a/packages/core/Cargo.toml
+++ b/packages/core/Cargo.toml
@@ -32,7 +32,7 @@ smallvec = "1.6"
slab = "0.4"
-futures-channel = "0.3"
+futures-channel = "0.3.21"
# used for noderefs
once_cell = "1.8"
diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs
deleted file mode 100644
index b7a9f513c..000000000
--- a/packages/core/examples/borrowed.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use dioxus::prelude::*;
-use dioxus_core as dioxus;
-use dioxus_core_macro::*;
-use dioxus_html as dioxus_elements;
-
-fn main() {}
-
-fn app(cx: Scope) -> Element {
- cx.render(rsx!(div {
- app2(
- p: "asd"
- )
- }))
-}
-
-#[derive(Props)]
-struct Borrowed<'a> {
- p: &'a str,
-}
-
-fn app2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element {
- let g = eat2(&cx);
- rsx!(cx, "")
-}
-
-fn eat2(s: &ScopeState) {}
-
-fn eat(f: &str) {}
-
-fn bleat() {
- let blah = String::from("asd");
- eat(&blah);
-}
diff --git a/packages/core/examples/component_children.rs b/packages/core/examples/component_children.rs
deleted file mode 100644
index ac76740b8..000000000
--- a/packages/core/examples/component_children.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-#![allow(non_snake_case)]
-
-use dioxus::prelude::*;
-use dioxus_core as dioxus;
-use dioxus_core_macro::*;
-use dioxus_html as dioxus_elements;
-
-fn main() {
- let mut dom = VirtualDom::new(parent);
- let edits = dom.rebuild();
- dbg!(edits);
-}
-
-fn parent(cx: Scope) -> Element {
- let value = cx.use_hook(|_| String::new());
-
- cx.render(rsx! {
- div {
- child(
- name: value,
- h1 {"hi"}
- )
- }
- })
-}
-
-#[derive(Props)]
-struct ChildProps<'a> {
- name: &'a str,
- children: Element<'a>,
-}
-
-fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
- cx.render(rsx! {
- div {
- "it's nested {cx.props.name}",
- &cx.props.children
- }
- })
-}
diff --git a/packages/core/examples/simple_hello.rs b/packages/core/examples/simple_hello.rs
deleted file mode 100644
index 94a897e7c..000000000
--- a/packages/core/examples/simple_hello.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use dioxus::prelude::*;
-use dioxus_core as dioxus;
-use dioxus_core_macro::*;
-use dioxus_html as dioxus_elements;
-
-// very tiny hello world
-fn main() {
- dioxus::VirtualDom::new(|cx| rsx!(cx, "hello world"));
-}
diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs
deleted file mode 100644
index 0de94b5c3..000000000
--- a/packages/core/examples/works.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-#![allow(non_snake_case)]
-
-use dioxus::prelude::*;
-use dioxus_core as dioxus;
-use dioxus_core_macro::*;
-use dioxus_html as dioxus_elements;
-
-fn main() {
- let _ = VirtualDom::new(parent);
-}
-
-fn parent(cx: Scope) -> Element {
- let value = cx.use_hook(|_| String::new());
-
- cx.render(rsx! {
- div {
- child( name: value )
- }
- })
-}
-
-#[derive(Props)]
-struct ChildProps<'a> {
- name: &'a str,
-}
-
-fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
- cx.render(rsx! {
- div {
- h1 { "it's nested" }
- grandchild( name: cx.props.name )
- }
- })
-}
-
-#[derive(Props)]
-struct Grandchild<'a> {
- name: &'a str,
-}
-
-fn grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
- cx.render(rsx! {
- div { "Hello {cx.props.name}!" }
- great_grandchild( name: cx.props.name )
- })
-}
-
-fn great_grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> {
- cx.render(rsx! {
- div {
- h1 { "it's nested" }
- }
- })
-}
diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs
index d7189a710..4e9a11c34 100644
--- a/packages/core/src/diff.rs
+++ b/packages/core/src/diff.rs
@@ -257,11 +257,10 @@ impl<'b> DiffState<'b> {
vcomponent.scope.set(Some(new_idx));
log::trace!(
- "created component \"{}\", id: {:?} parent {:?} orig: {:?}",
+ "created component \"{}\", id: {:?} parent {:?}",
vcomponent.fn_name,
new_idx,
parent_idx,
- vcomponent.originator
);
// if vcomponent.can_memoize {
@@ -853,7 +852,7 @@ impl<'b> DiffState<'b> {
nodes_created += self.create_node(new_node);
} else {
self.diff_node(&old[old_index], new_node);
- nodes_created += self.push_all_nodes(new_node);
+ nodes_created += self.push_all_real_nodes(new_node);
}
}
@@ -876,7 +875,7 @@ impl<'b> DiffState<'b> {
nodes_created += self.create_node(new_node);
} else {
self.diff_node(&old[old_index], new_node);
- nodes_created += self.push_all_nodes(new_node);
+ nodes_created += self.push_all_real_nodes(new_node);
}
}
@@ -899,7 +898,7 @@ impl<'b> DiffState<'b> {
nodes_created += self.create_node(new_node);
} else {
self.diff_node(&old[old_index], new_node);
- nodes_created += self.push_all_nodes(new_node);
+ nodes_created += self.push_all_real_nodes(new_node);
}
}
@@ -957,13 +956,6 @@ impl<'b> DiffState<'b> {
let props = scope.props.take().unwrap();
c.props.borrow_mut().replace(props);
self.scopes.try_remove(scope_id).unwrap();
-
- // // we can only remove components if they are actively being diffed
- // if self.scope_stack.contains(&c.originator) {
- // log::trace!("Removing component {:?}", old);
-
- // self.scopes.try_remove(scope_id).unwrap();
- // }
}
self.leave_scope();
}
@@ -1100,9 +1092,9 @@ impl<'b> DiffState<'b> {
}
// recursively push all the nodes of a tree onto the stack and return how many are there
- fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize {
+ fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
match node {
- VNode::Text(_) | VNode::Placeholder(_) => {
+ VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => {
self.mutations.push_root(node.mounted_id());
1
}
@@ -1110,7 +1102,7 @@ impl<'b> DiffState<'b> {
VNode::Fragment(frag) => {
let mut added = 0;
for child in frag.children {
- added += self.push_all_nodes(child);
+ added += self.push_all_real_nodes(child);
}
added
}
@@ -1118,16 +1110,7 @@ impl<'b> DiffState<'b> {
VNode::Component(c) => {
let scope_id = c.scope.get().unwrap();
let root = self.scopes.root_node(scope_id);
- self.push_all_nodes(root)
- }
-
- VNode::Element(el) => {
- let mut num_on_stack = 0;
- for child in el.children.iter() {
- num_on_stack += self.push_all_nodes(child);
- }
- self.mutations.push_root(el.id.get().unwrap());
- num_on_stack + 1
+ self.push_all_real_nodes(root)
}
}
}
diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs
index 8386cedd8..1a9899e4a 100644
--- a/packages/core/src/events.rs
+++ b/packages/core/src/events.rs
@@ -119,24 +119,41 @@ pub enum EventPriority {
Low = 0,
}
+/// The internal Dioxus type that carries any event data to the relevant handler.
+///
+///
+///
+///
+///
pub struct AnyEvent {
pub(crate) bubble_state: Rc,
pub(crate) data: Arc,
}
impl AnyEvent {
+ /// Convert this AnyEvent into a specific UiEvent with EventData.
+ ///
+ /// ```rust, ignore
+ /// let evt: FormEvent = evvt.downcast().unwrap();
+ /// ```
pub fn downcast(self) -> Option> {
let AnyEvent { data, bubble_state } = self;
- if let Ok(data) = data.downcast::() {
- Some(UiEvent { bubble_state, data })
- } else {
- None
- }
+ data.downcast::()
+ .ok()
+ .map(|data| UiEvent { bubble_state, data })
}
}
+/// A UiEvent is a type that wraps various EventData.
+///
+/// You should prefer to use the name of the event directly, rather than
+/// the UiEvent generic type.
+///
+/// For the HTML crate, this would include [`MouseEvent`], [`FormEvent`] etc.
pub struct UiEvent {
+ /// The internal data of the event
+ /// This is wrapped in an Arc so that it can be sent across threads
pub data: Arc,
#[allow(unused)]
diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs
index 1340c916c..5fac7b194 100644
--- a/packages/core/src/lazynodes.rs
+++ b/packages/core/src/lazynodes.rs
@@ -35,14 +35,32 @@ enum StackNodeStorage<'a, 'b> {
}
impl<'a, 'b> LazyNodes<'a, 'b> {
- pub fn new_some(_val: F) -> Self
- where
- F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
- {
- Self::new(_val)
+ /// Create a new LazyNodes closure, optimistically placing it onto the stack.
+ ///
+ /// If the closure cannot fit into the stack allocation (16 bytes), then it
+ /// is placed on the heap. Most closures will fit into the stack, and is
+ /// the most optimal way to use the creation function.
+ pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
+ // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
+ let mut slot = Some(val);
+
+ let val = move |fac: Option>| {
+ let inner = slot.take().unwrap();
+ fac.map(inner)
+ };
+
+ // miri does not know how to work with mucking directly into bytes
+ // just use a heap allocated type when miri is running
+ if cfg!(miri) {
+ Self {
+ inner: StackNodeStorage::Heap(Box::new(val)),
+ }
+ } else {
+ unsafe { LazyNodes::new_inner(val) }
+ }
}
- /// force this call onto the stack
+ /// Create a new LazyNodes closure, but force it onto the heap.
pub fn new_boxed(_val: F) -> Self
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
@@ -60,25 +78,6 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
}
}
- pub fn new(_val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
- // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
- let mut slot = Some(_val);
-
- let val = move |fac: Option>| {
- let inner = slot.take().unwrap();
- fac.map(inner)
- };
-
- // miri does not know how to work with mucking directly into bytes
- if cfg!(miri) {
- Self {
- inner: StackNodeStorage::Heap(Box::new(val)),
- }
- } else {
- unsafe { LazyNodes::new_inner(val) }
- }
- }
-
unsafe fn new_inner(val: F) -> Self
where
F: FnMut(Option>) -> Option> + 'b,
@@ -153,6 +152,15 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
}
}
+ /// Call the closure with the given factory to produce real VNodes.
+ ///
+ /// ```rust, ignore
+ /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
+ ///
+ /// let fac = NodeFactory::new(&cx);
+ ///
+ /// let node = f.call(cac);
+ /// ```
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
match self.inner {
StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(),
@@ -245,9 +253,7 @@ mod tests {
#[test]
fn it_works() {
fn app(cx: Scope<()>) -> Element {
- cx.render(LazyNodes::new_some(|f| {
- f.text(format_args!("hello world!"))
- }))
+ cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
let mut dom = VirtualDom::new(app);
@@ -280,7 +286,7 @@ mod tests {
.map(|i| {
let val = cx.props.inner.clone();
- LazyNodes::new_some(move |f| {
+ LazyNodes::new(move |f| {
log::debug!("hell closure");
let inner = DropInner { id: i };
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
@@ -288,7 +294,7 @@ mod tests {
})
.collect::>();
- LazyNodes::new_some(|f| {
+ LazyNodes::new(|f| {
log::debug!("main closure");
f.fragment_from_iter(it)
})
diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs
index e81ddca87..07fb5ce79 100644
--- a/packages/core/src/lib.rs
+++ b/packages/core/src/lib.rs
@@ -1,5 +1,6 @@
#![allow(non_snake_case)]
#![doc = include_str!("../README.md")]
+#![deny(missing_docs)]
pub(crate) mod diff;
pub(crate) mod events;
@@ -76,6 +77,9 @@ pub use crate::innerlude::{
VElement, VFragment, VNode, VPlaceholder, VText, VirtualDom,
};
+/// The purpose of this module is to alleviate imports of many common types
+///
+/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::innerlude::{
fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment,
diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs
index bd94d0b71..2d3a4505f 100644
--- a/packages/core/src/mutations.rs
+++ b/packages/core/src/mutations.rs
@@ -16,8 +16,13 @@ use std::{any::Any, fmt::Debug};
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
pub struct Mutations<'a> {
+ /// The list of edits that need to be applied for the RealDOM to match the VirtualDOM.
pub edits: Vec>,
+
+ /// The list of Scopes that were diffed, created, and removed during the Diff process.
pub dirty_scopes: FxHashSet,
+
+ /// The list of nodes to connect to the RealDOM.
pub refs: Vec>,
}
@@ -39,74 +44,146 @@ impl Debug for Mutations<'_> {
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
+ /// Push the given root node onto our stack.
PushRoot {
+ /// The ID of the root node to push.
root: u64,
},
+ /// Pop the topmost node from our stack and append them to the node
+ /// at the top of the stack.
AppendChildren {
+ /// How many nodes should be popped from the stack.
+ /// The node remaining on the stack will be the target for the append.
many: u32,
},
- // // save a possibly-fragment node as a template
- // SaveAsTemplate {
- // many: u32,
- // },
-
- // "Root" refers to the item directly
- // it's a waste of an instruction to push the root directly
+ /// Replace a given (single) node with a handful of nodes currently on the stack.
ReplaceWith {
+ /// The ID of the node to be replaced.
root: u64,
+
+ /// How many nodes should be popped from the stack to replace the target node.
m: u32,
},
+
+ /// Insert a number of nodes after a given node.
InsertAfter {
+ /// The ID of the node to insert after.
root: u64,
+
+ /// How many nodes should be popped from the stack to insert after the target node.
n: u32,
},
+
+ /// Insert a number of nodes before a given node.
InsertBefore {
+ /// The ID of the node to insert before.
root: u64,
+
+ /// How many nodes should be popped from the stack to insert before the target node.
n: u32,
},
+
+ /// Remove a particular node from the DOM
Remove {
+ /// The ID of the node to remove.
root: u64,
},
+
+ /// Create a new purely-text node
CreateTextNode {
+ /// The ID the new node should have.
+ root: u64,
+
+ /// The textcontent of the node
text: &'bump str,
- root: u64,
},
+
+ /// Create a new purely-element node
CreateElement {
- tag: &'bump str,
+ /// The ID the new node should have.
root: u64,
+
+ /// The tagname of the node
+ tag: &'bump str,
},
+
+ /// Create a new purely-comment node with a given namespace
CreateElementNs {
- tag: &'bump str,
+ /// The ID the new node should have.
root: u64,
+
+ /// The namespace of the node
+ tag: &'bump str,
+
+ /// The namespace of the node (like `SVG`)
ns: &'static str,
},
+
+ /// Create a new placeholder node.
+ /// In most implementations, this will either be a hidden div or a comment node.
CreatePlaceholder {
+ /// The ID the new node should have.
root: u64,
},
+
+ /// Create a new Event Listener.
NewEventListener {
+ /// The name of the event to listen for.
event_name: &'static str,
+
+ /// The ID of the node to attach the listener to.
scope: ScopeId,
+
+ /// The ID of the node to attach the listener to.
root: u64,
},
+
+ /// Remove an existing Event Listener.
RemoveEventListener {
+ /// The ID of the node to remove.
root: u64,
+
+ /// The name of the event to remove.
event: &'static str,
},
+
+ /// Set the textcontent of a node.
SetText {
+ /// The ID of the node to set the textcontent of.
root: u64,
+
+ /// The textcontent of the node
text: &'bump str,
},
+
+ /// Set the value of a node's attribute.
SetAttribute {
+ /// The ID of the node to set the attribute of.
root: u64,
+
+ /// The name of the attribute to set.
field: &'static str,
+
+ /// The value of the attribute.
value: &'bump str,
+
+ /// The (optional) namespace of the attribute.
+ /// For instance, "style" is in the "style" namespace.
ns: Option<&'bump str>,
},
+
+ /// Remove an attribute from a node.
RemoveAttribute {
+ /// The ID of the node to remove.
root: u64,
+
+ /// The name of the attribute to remove.
name: &'static str,
+
+ /// The namespace of the attribute.
+ ns: Option<&'bump str>,
},
}
@@ -218,8 +295,15 @@ impl<'a> Mutations<'a> {
}
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
- let name = attribute.name;
- self.edits.push(RemoveAttribute { name, root });
+ let Attribute {
+ name, namespace, ..
+ } = attribute;
+
+ self.edits.push(RemoveAttribute {
+ name,
+ ns: *namespace,
+ root,
+ });
}
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs
index 7a36f58ce..094af2281 100644
--- a/packages/core/src/nodes.rs
+++ b/packages/core/src/nodes.rs
@@ -148,7 +148,7 @@ impl<'src> VNode<'src> {
}
// Create an "owned" version of the vnode.
- pub fn decouple(&self) -> VNode<'src> {
+ pub(crate) fn decouple(&self) -> VNode<'src> {
match *self {
VNode::Text(t) => VNode::Text(t),
VNode::Element(e) => VNode::Element(e),
@@ -181,7 +181,6 @@ impl Debug for VNode<'_> {
.field("fnptr", &comp.user_fc)
.field("key", &comp.key)
.field("scope", &comp.scope)
- .field("originator", &comp.originator)
.finish(),
}
}
@@ -200,6 +199,7 @@ impl std::fmt::Display for ElementId {
}
impl ElementId {
+ /// Convertt the ElementId to a `u64`.
pub fn as_u64(self) -> u64 {
self.0 as u64
}
@@ -211,18 +211,26 @@ fn empty_cell() -> Cell> {
/// A placeholder node only generated when Fragments don't have any children.
pub struct VPlaceholder {
+ /// The [`ElementId`] of the placeholder.
pub id: Cell >,
}
/// A bump-allocated string slice and metadata.
pub struct VText<'src> {
- pub text: &'src str,
+ /// The [`ElementId`] of the VText.
pub id: Cell >,
+
+ /// The text of the VText.
+ pub text: &'src str,
+
+ /// An indiciation if this VText can be ignored during diffing
+ /// Is usually only when there are no strings to be formatted (so the text is &'static str)
pub is_static: bool,
}
/// A list of VNodes with no single root.
pub struct VFragment<'src> {
+ /// The key of the fragment to be used during keyed diffing.
pub key: Option<&'src str>,
/// Fragments can never have zero children. Enforced by NodeFactory.
@@ -233,13 +241,34 @@ pub struct VFragment<'src> {
/// An element like a "div" with children, listeners, and attributes.
pub struct VElement<'a> {
- pub tag: &'static str,
- pub namespace: Option<&'static str>,
- pub key: Option<&'a str>,
+ /// The [`ElementId`] of the VText.
pub id: Cell >,
+
+ /// The key of the element to be used during keyed diffing.
+ pub key: Option<&'a str>,
+
+ /// The tag name of the element.
+ ///
+ /// IE "div"
+ pub tag: &'static str,
+
+ /// The namespace of the VElement
+ ///
+ /// IE "svg"
+ pub namespace: Option<&'static str>,
+
+ /// The parent of the Element (if any).
+ ///
+ /// Used when bubbling events
pub parent: Cell >,
+
+ /// The Listeners of the VElement.
pub listeners: &'a [Listener<'a>],
+
+ /// The attributes of the VElement.
pub attributes: &'a [Attribute<'a>],
+
+ /// The children of the VElement.
pub children: &'a [VNode<'a>],
}
@@ -275,12 +304,19 @@ impl Debug for VElement<'_> {
/// };
/// ```
pub trait DioxusElement {
+ /// The tag name of the element.
const TAG_NAME: &'static str;
+
+ /// The namespace of the element.
const NAME_SPACE: Option<&'static str>;
+
+ /// The tag name of the element.
#[inline]
fn tag_name(&self) -> &'static str {
Self::TAG_NAME
}
+
+ /// The namespace of the element.
#[inline]
fn namespace(&self) -> Option<&'static str> {
Self::NAME_SPACE
@@ -291,16 +327,25 @@ pub trait DioxusElement {
/// `href="https://example.com"`.
#[derive(Clone, Debug)]
pub struct Attribute<'a> {
+ /// The name of the attribute.
pub name: &'static str,
+ /// The value of the attribute.
pub value: &'a str,
+ /// An indication if this attribute can be ignored during diffing
+ ///
+ /// Usually only when there are no strings to be formatted (so the value is &'static str)
pub is_static: bool,
+ /// An indication of we should always try and set the attribute.
+ /// Used in controlled components to ensure changes are propagated.
pub is_volatile: bool,
- // Doesn't exist in the html spec.
- // Used in Dioxus to denote "style" tags.
+ /// The namespace of the attribute.
+ ///
+ /// Doesn't exist in the html spec.
+ /// Used in Dioxus to denote "style" tags and other attribute groups.
pub namespace: Option<&'static str>,
}
@@ -353,6 +398,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
///
/// ```
pub struct EventHandler<'bump, T = ()> {
+ /// The (optional) callback that the user specified
+ /// Uses a `RefCell` to allow for interior mutability, and FnMut closures.
pub callback: RefCell >>,
}
@@ -381,12 +428,23 @@ impl EventHandler<'_, T> {
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
pub struct VComponent<'src> {
+ /// The key of the component to be used during keyed diffing.
pub key: Option<&'src str>,
- pub originator: ScopeId,
+
+ /// The ID of the component.
+ /// Will not be assigned until after the component has been initialized.
pub scope: Cell>,
+
+ /// An indication if the component is static (can be memozied)
pub can_memoize: bool,
+
+ /// The function pointer to the component's render function.
pub user_fc: ComponentPtr,
+
+ /// The actual name of the component.
pub fn_name: &'static str,
+
+ /// The props of the component.
pub props: RefCell >>,
}
@@ -434,6 +492,7 @@ pub struct NodeFactory<'a> {
}
impl<'a> NodeFactory<'a> {
+ /// Create a new [`NodeFactory`] from a [`Scope`] or [`ScopeState`]
pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> {
NodeFactory {
scope,
@@ -441,6 +500,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Get the custom alloactor for this component
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
self.bump
@@ -482,6 +542,7 @@ impl<'a> NodeFactory<'a> {
}))
}
+ /// Create a new [`VNode::VElement`]
pub fn element(
&self,
el: impl DioxusElement,
@@ -500,6 +561,9 @@ impl<'a> NodeFactory<'a> {
)
}
+ /// Create a new [`VNode::VElement`] without the trait bound
+ ///
+ /// IE pass in "div" instead of `div`
pub fn raw_element(
&self,
tag_name: &'static str,
@@ -529,6 +593,7 @@ impl<'a> NodeFactory<'a> {
}))
}
+ /// Create a new [`Attribute`]
pub fn attr(
&self,
name: &'static str,
@@ -546,6 +611,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Create a new [`VNode::VComponent`]
pub fn component(
&self,
component: fn(Scope<'a, P>) -> Element,
@@ -561,11 +627,8 @@ impl<'a> NodeFactory<'a> {
scope: Default::default(),
can_memoize: P::IS_STATIC,
user_fc: component as ComponentPtr,
- originator: self.scope.scope_id(),
fn_name,
props: RefCell::new(Some(Box::new(VComponentProps {
- // local_props: RefCell::new(Some(props)),
- // heap_props: RefCell::new(None),
props,
memo: P::memoize, // smuggle the memoization function across borders
@@ -586,6 +649,7 @@ impl<'a> NodeFactory<'a> {
VNode::Component(vcomp)
}
+ /// Create a new [`Listener`]
pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> {
Listener {
event,
@@ -594,6 +658,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Create a new [`VNode::VFragment`] from a root of the rsx! call
pub fn fragment_root<'b, 'c>(
self,
node_iter: impl IntoIterator- + 'c> + 'b,
@@ -614,6 +679,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Create a new [`VNode::VFragment`] from any iterator
pub fn fragment_from_iter<'b, 'c>(
self,
node_iter: impl IntoIterator
- + 'c> + 'b,
@@ -653,8 +719,7 @@ impl<'a> NodeFactory<'a> {
}
}
- // this isn't quite feasible yet
- // I think we need some form of interior mutability or state on nodefactory that stores which subtree was created
+ /// Create a new [`VNode`] from any iterator of children
pub fn create_children(
self,
node_iter: impl IntoIterator
- >,
@@ -679,6 +744,7 @@ impl<'a> NodeFactory<'a> {
}
}
+ /// Create a new [`EventHandler`] from an [`FnMut`]
pub fn event_handler
(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> {
let handler: &mut dyn FnMut(T) = self.bump.alloc(f);
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
@@ -709,6 +775,7 @@ impl Debug for NodeFactory<'_> {
/// As such, all node creation must go through the factory, which is only available in the component context.
/// These strict requirements make it possible to manage lifetimes and state.
pub trait IntoVNode<'a> {
+ /// Convert this into a [`VNode`], using the [`NodeFactory`] as a source of allocation
fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>;
}
diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs
index f42bc36ba..e81fc52a2 100644
--- a/packages/core/src/properties.rs
+++ b/packages/core/src/properties.rs
@@ -128,8 +128,14 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
/// }
/// ```
pub trait Properties: Sized {
+ /// The type of the builder for this component.
+ /// Used to create "in-progress" versions of the props.
type Builder;
+
+ /// An indication if these props are can be memoized automatically.
const IS_STATIC: bool;
+
+ /// Create a builder for this component.
fn builder() -> Self::Builder;
/// Memoization can only happen if the props are valid for the 'static lifetime
diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs
index 1e58dcfd2..65725ad4f 100644
--- a/packages/core/src/scopes.rs
+++ b/packages/core/src/scopes.rs
@@ -11,6 +11,7 @@ use std::{
future::Future,
pin::Pin,
rc::Rc,
+ sync::Arc,
};
/// for traceability, we use the raw fn pointer to identify the function
@@ -96,11 +97,10 @@ impl ScopeArena {
// Get the height of the scope
let height = parent_scope
- .map(|id| self.get_scope(id).map(|scope| scope.height + 1))
- .flatten()
+ .and_then(|id| self.get_scope(id).map(|scope| scope.height + 1))
.unwrap_or_default();
- let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten();
+ let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f));
/*
This scopearena aggressively reuses old scopes when possible.
@@ -399,7 +399,10 @@ impl ScopeArena {
/// }
/// ```
pub struct Scope<'a, P = ()> {
+ /// The internal ScopeState for this component
pub scope: &'a ScopeState,
+
+ /// The props for this component
pub props: &'a P,
}
@@ -582,12 +585,17 @@ impl ScopeState {
self.our_arena_idx
}
+ /// Get a handle to the raw update scheduler channel
+ pub fn scheduler_channel(&self) -> UnboundedSender {
+ self.tasks.sender.clone()
+ }
+
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using prepare_update and get_scope_id
- pub fn schedule_update(&self) -> Rc {
+ pub fn schedule_update(&self) -> Arc {
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
- Rc::new(move || {
+ Arc::new(move || {
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
@@ -597,9 +605,9 @@ impl ScopeState {
/// A component's ScopeId can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
///
/// This method should be used when you want to schedule an update for a component
- pub fn schedule_update_any(&self) -> Rc {
+ pub fn schedule_update_any(&self) -> Arc {
let chan = self.tasks.sender.clone();
- Rc::new(move |id| {
+ Arc::new(move |id| {
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
})
}
@@ -656,8 +664,7 @@ impl ScopeState {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::(), value.clone())
- .map(|f| f.downcast::().ok())
- .flatten();
+ .and_then(|f| f.downcast::().ok());
value
}
@@ -687,8 +694,7 @@ impl ScopeState {
self.shared_contexts
.borrow_mut()
.insert(TypeId::of::(), value.clone())
- .map(|f| f.downcast::().ok())
- .flatten();
+ .and_then(|f| f.downcast::().ok());
return value;
}
@@ -750,7 +756,8 @@ impl ScopeState {
self.push_future(fut);
}
- // todo: attach some state to the future to know if we should poll it
+ /// Informs the scheduler that this task is no longer needed and should be removed
+ /// on next poll.
pub fn remove_future(&self, id: TaskId) {
self.tasks.remove_fut(id);
}
diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs
index b9640792d..a6a524e27 100644
--- a/packages/core/src/util.rs
+++ b/packages/core/src/util.rs
@@ -1,5 +1,7 @@
use crate::innerlude::*;
+/// An iterator that only yields "real" [`Element`]s. IE only Elements that are
+/// not [`VNode::VComponent`] or [`VNode::VFragment`], .
pub struct ElementIdIterator<'a> {
vdom: &'a VirtualDom,
@@ -9,6 +11,9 @@ pub struct ElementIdIterator<'a> {
}
impl<'a> ElementIdIterator<'a> {
+ /// Create a new iterator from the given [`VirtualDom`] and [`VNode`]
+ ///
+ /// This will allow you to iterate through all the real childrne of the [`VNode`].
pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self {
Self {
vdom,
diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs
index a7092104c..6d1678773 100644
--- a/packages/core/src/virtual_dom.rs
+++ b/packages/core/src/virtual_dom.rs
@@ -114,15 +114,18 @@ pub struct VirtualDom {
),
}
+/// The type of message that can be sent to the scheduler.
+///
+/// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)]
pub enum SchedulerMsg {
- // events from the host
+ /// Events from the Renderer
Event(UserEvent),
- // setstate
+ /// Immediate updates from Components that mark them as dirty
Immediate(ScopeId),
- // an async task pushed from an event handler (or just spawned)
+ /// New tasks from components that should be polled when the next poll is ready
NewTask(ScopeId),
}
@@ -388,6 +391,9 @@ impl VirtualDom {
}
}
+ /// Handle an individual message for the scheduler.
+ ///
+ /// This will either call an event listener or mark a component as dirty.
pub fn process_message(&mut self, msg: SchedulerMsg) {
match msg {
SchedulerMsg::NewTask(_id) => {
diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs
index f977ba96e..35058d418 100644
--- a/packages/core/tests/lifecycle.rs
+++ b/packages/core/tests/lifecycle.rs
@@ -1,4 +1,5 @@
#![allow(unused, non_upper_case_globals)]
+#![allow(non_snake_case)]
//! Tests for the lifecycle of components.
use dioxus::prelude::*;
diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs
index 8ce030147..fe98c83ae 100644
--- a/packages/core/tests/miri_stress.rs
+++ b/packages/core/tests/miri_stress.rs
@@ -1,3 +1,4 @@
+#![allow(non_snake_case)]
/*
Stress Miri as much as possible.
@@ -311,22 +312,11 @@ fn leak_thru_children() {
#[test]
fn test_pass_thru() {
- #[inline_props]
- fn Router<'a>(cx: Scope, children: Element<'a>) -> Element {
- cx.render(rsx! {
- div {
- &cx.props.children
- }
- })
- }
-
#[inline_props]
fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! {
header {
- nav {
- &cx.props.children
- }
+ nav { children }
}
})
}
@@ -365,9 +355,9 @@ fn test_pass_thru() {
class: "columns is-mobile",
div {
class: "column is-full",
- &cx.props.nav,
- &cx.props.body,
- &cx.props.footer,
+ nav,
+ body,
+ footer,
}
}
})
@@ -398,7 +388,7 @@ fn test_pass_thru() {
let mut dom = new_dom(app, ());
let _ = dom.rebuild();
- for x in 0..40 {
+ for _ in 0..40 {
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
dom.work_with_deadline(|| false);
dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
diff --git a/packages/core/tests/passthru.rs b/packages/core/tests/passthru.rs
index e00aa545c..948de8b67 100644
--- a/packages/core/tests/passthru.rs
+++ b/packages/core/tests/passthru.rs
@@ -1,4 +1,5 @@
#![allow(unused, non_upper_case_globals)]
+#![allow(non_snake_case)]
//! Diffing Tests
//!
@@ -40,9 +41,7 @@ fn nested_passthru_creates() {
#[inline_props]
fn Child<'a>(cx: Scope, children: Element<'a>) -> Element {
- cx.render(rsx! {
- children
- })
+ cx.render(rsx! { children })
};
let mut dom = VirtualDom::new(app);
diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs
index 70e823569..18d569914 100644
--- a/packages/core/tests/sharedstate.rs
+++ b/packages/core/tests/sharedstate.rs
@@ -1,4 +1,4 @@
-#![allow(unused, non_upper_case_globals)]
+#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId};
use dioxus_core as dioxus;
diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml
index fa1bc88dc..017d399c1 100644
--- a/packages/desktop/Cargo.toml
+++ b/packages/desktop/Cargo.toml
@@ -13,31 +13,39 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] }
-argh = "0.1.4"
-serde = "1.0.120"
-serde_json = "1.0.61"
-thiserror = "1.0.23"
-log = "0.4.13"
-html-escape = "0.2.9"
-wry = "0.12.2"
-futures-channel = "0.3"
-tokio = { version = "1.12.0", features = [
+serde = "1.0.136"
+serde_json = "1.0.79"
+thiserror = "1.0.30"
+log = "0.4.14"
+wry = { version = "0.13.1" }
+futures-channel = "0.3.21"
+tokio = { version = "1.16.1", features = [
"sync",
"rt-multi-thread",
"rt",
"time",
], optional = true, default-features = false }
-dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" }
webbrowser = "0.5.5"
mime_guess = "2.0.3"
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" }
-
+dunce = "1.0.2"
+[target.'cfg(target_os = "macos")'.dependencies]
+core-foundation = "0.9.3"
[features]
default = ["tokio_runtime"]
tokio_runtime = ["tokio"]
+devtool = ["wry/devtool"]
+fullscreen = ["wry/fullscreen"]
+transparent = ["wry/transparent"]
+
+tray = ["wry/tray"]
+ayatana = ["wry/ayatana"]
+
[dev-dependencies]
+dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
dioxus-hooks = { path = "../hooks" }
+# image = "0.24.0" # enable this when generating a new desktop image
diff --git a/packages/desktop/src/assets/default_icon.bin b/packages/desktop/src/assets/default_icon.bin
new file mode 100644
index 000000000..2d4898f0c
Binary files /dev/null and b/packages/desktop/src/assets/default_icon.bin differ
diff --git a/packages/desktop/src/assets/default_icon.png b/packages/desktop/src/assets/default_icon.png
new file mode 100644
index 000000000..d0742ea8a
Binary files /dev/null and b/packages/desktop/src/assets/default_icon.png differ
diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs
index 5baa46f5d..6028706bc 100644
--- a/packages/desktop/src/cfg.rs
+++ b/packages/desktop/src/cfg.rs
@@ -1,3 +1,6 @@
+use std::path::PathBuf;
+
+use wry::application::window::Icon;
use wry::{
application::{
event_loop::EventLoop,
@@ -11,14 +14,16 @@ use wry::{
pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
pub struct DesktopConfig {
- pub window: WindowBuilder,
- pub file_drop_handler: Option bool>>,
- pub protocos: Vec,
+ pub(crate) window: WindowBuilder,
+ pub(crate) file_drop_handler: Option bool>>,
+ pub(crate) protocols: Vec,
pub(crate) pre_rendered: Option,
pub(crate) event_handler: Option>,
+ pub(crate) disable_context_menu: bool,
+ pub(crate) resource_dir: Option,
}
-pub type WryProtocl = (
+pub(crate) type WryProtocol = (
String,
Box WryResult + 'static>,
);
@@ -28,15 +33,31 @@ impl DesktopConfig {
#[inline]
pub fn new() -> Self {
let window = WindowBuilder::new().with_title("Dioxus app");
+
Self {
event_handler: None,
window,
- protocos: Vec::new(),
+ protocols: Vec::new(),
file_drop_handler: None,
pre_rendered: None,
+ disable_context_menu: !cfg!(debug_assertions),
+ resource_dir: None,
}
}
+ /// set the directory from which assets will be searched in release mode
+ pub fn with_resource_directory(mut self, path: impl Into) -> Self {
+ self.resource_dir = Some(path.into());
+ self
+ }
+
+ /// Set whether or not the right-click context menu should be disabled.
+ pub fn with_disable_context_menu(&mut self, disable: bool) -> &mut Self {
+ self.disable_context_menu = disable;
+ self
+ }
+
+ /// With pre-rendered HTML content
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
self.pre_rendered = Some(content);
self
@@ -75,7 +96,21 @@ impl DesktopConfig {
where
F: Fn(&HttpRequest) -> WryResult + 'static,
{
- self.protocos.push((name, Box::new(handler)));
+ self.protocols.push((name, Box::new(handler)));
+ self
+ }
+
+ pub fn with_icon(&mut self, icon: Icon) -> &mut Self {
+ self.window.window.window_icon = Some(icon);
+ self
+ }
+}
+
+impl DesktopConfig {
+ pub(crate) fn with_default_icon(mut self) -> Self {
+ let bin: &[u8] = include_bytes!("./assets/default_icon.bin");
+ let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed");
+ self.window.window.window_icon = Some(rgba);
self
}
}
@@ -85,3 +120,31 @@ impl Default for DesktopConfig {
Self::new()
}
}
+
+// dirty trick, avoid introducing `image` at runtime
+// TODO: use serde when `Icon` impl serde
+//
+// This function should only be enabled when generating new icons.
+//
+// #[test]
+// #[ignore]
+// fn prepare_default_icon() {
+// use image::io::Reader as ImageReader;
+// use image::ImageFormat;
+// use std::fs::File;
+// use std::io::Cursor;
+// use std::io::Write;
+// use std::path::PathBuf;
+// let png: &[u8] = include_bytes!("default_icon.png");
+// let mut reader = ImageReader::new(Cursor::new(png));
+// reader.set_format(ImageFormat::Png);
+// let icon = reader.decode().unwrap();
+// let bin = PathBuf::from(file!())
+// .parent()
+// .unwrap()
+// .join("default_icon.bin");
+// println!("{:?}", bin);
+// let mut file = File::create(bin).unwrap();
+// file.write_all(icon.as_bytes()).unwrap();
+// println!("({}, {})", icon.width(), icon.height())
+// }
diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs
new file mode 100644
index 000000000..ef2ecf5c1
--- /dev/null
+++ b/packages/desktop/src/controller.rs
@@ -0,0 +1,106 @@
+use crate::desktop_context::{DesktopContext, UserWindowEvent};
+use dioxus_core::*;
+use std::{
+ collections::{HashMap, VecDeque},
+ sync::atomic::AtomicBool,
+ sync::{Arc, RwLock},
+};
+use wry::{
+ self,
+ application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId},
+ webview::WebView,
+};
+
+pub(super) struct DesktopController {
+ pub(super) webviews: HashMap,
+ pub(super) sender: futures_channel::mpsc::UnboundedSender,
+ pub(super) pending_edits: Arc>>,
+ pub(super) quit_app_on_close: bool,
+ pub(super) is_ready: Arc,
+}
+
+impl DesktopController {
+ // Launch the virtualdom on its own thread managed by tokio
+ // returns the desktop state
+ pub(super) fn new_on_tokio(
+ root: Component,
+ props: P,
+ proxy: EventLoopProxy,
+ ) -> Self {
+ let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
+ let pending_edits = edit_queue.clone();
+
+ let (sender, receiver) = futures_channel::mpsc::unbounded::();
+ let return_sender = sender.clone();
+
+ let desktop_context_proxy = proxy.clone();
+ std::thread::spawn(move || {
+ // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
+ let runtime = tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .unwrap();
+
+ runtime.block_on(async move {
+ let mut dom =
+ VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
+
+ let window_context = DesktopContext::new(desktop_context_proxy);
+
+ dom.base_scope().provide_context(window_context);
+
+ let edits = dom.rebuild();
+
+ edit_queue
+ .write()
+ .unwrap()
+ .push_front(serde_json::to_string(&edits.edits).unwrap());
+
+ // Make sure the window is ready for any new updates
+ proxy.send_event(UserWindowEvent::Update).unwrap();
+
+ loop {
+ dom.wait_for_work().await;
+ let mut muts = dom.work_with_deadline(|| false);
+
+ while let Some(edit) = muts.pop() {
+ edit_queue
+ .write()
+ .unwrap()
+ .push_front(serde_json::to_string(&edit.edits).unwrap());
+ }
+
+ let _ = proxy.send_event(UserWindowEvent::Update);
+ }
+ })
+ });
+
+ Self {
+ pending_edits,
+ sender: return_sender,
+ webviews: HashMap::new(),
+ is_ready: Arc::new(AtomicBool::new(false)),
+ quit_app_on_close: true,
+ }
+ }
+
+ pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
+ self.webviews.remove(&window_id);
+
+ if self.webviews.is_empty() && self.quit_app_on_close {
+ *control_flow = ControlFlow::Exit;
+ }
+ }
+
+ pub(super) fn try_load_ready_webviews(&mut self) {
+ if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
+ let mut queue = self.pending_edits.write().unwrap();
+ let (_id, view) = self.webviews.iter_mut().next().unwrap();
+
+ while let Some(edit) = queue.pop_back() {
+ view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
+ .unwrap();
+ }
+ }
+ }
+}
diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs
index 9c0404477..eaff13750 100644
--- a/packages/desktop/src/desktop_context.rs
+++ b/packages/desktop/src/desktop_context.rs
@@ -3,13 +3,23 @@ use std::rc::Rc;
use dioxus_core::ScopeState;
use wry::application::event_loop::EventLoopProxy;
-use crate::UserWindowEvent;
+use UserWindowEvent::*;
-type ProxyType = EventLoopProxy;
+pub type ProxyType = EventLoopProxy;
-/// Desktop-Window handle api context
+/// Get an imperative handle to the current window
+pub fn use_window(cx: &ScopeState) -> &Rc {
+ cx.use_hook(|_| cx.consume_context::())
+ .as_ref()
+ .unwrap()
+}
+
+/// An imperative interface to the current window.
///
-/// you can use this context control some window event
+/// To get a handle to the current window, use the [`use_window`] hook.
+///
+///
+/// # Example
///
/// you can use `cx.consume_context::` to get this context
///
@@ -18,7 +28,8 @@ type ProxyType = EventLoopProxy;
/// ```
#[derive(Clone)]
pub struct DesktopContext {
- proxy: ProxyType,
+ /// The wry/tao proxy to the current window
+ pub proxy: ProxyType,
}
impl DesktopContext {
@@ -35,33 +46,148 @@ impl DesktopContext {
/// onmousedown: move |_| { desktop.drag_window(); }
/// ```
pub fn drag(&self) {
- let _ = self.proxy.send_event(UserWindowEvent::DragWindow);
+ let _ = self.proxy.send_event(DragWindow);
}
/// set window minimize state
- pub fn minimize(&self, minimized: bool) {
- let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized));
+ pub fn set_minimized(&self, minimized: bool) {
+ let _ = self.proxy.send_event(Minimize(minimized));
}
/// set window maximize state
- pub fn maximize(&self, maximized: bool) {
- let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized));
+ pub fn set_maximized(&self, maximized: bool) {
+ let _ = self.proxy.send_event(Maximize(maximized));
+ }
+
+ /// toggle window maximize state
+ pub fn toggle_maximized(&self) {
+ let _ = self.proxy.send_event(MaximizeToggle);
+ }
+
+ /// set window visible or not
+ pub fn set_visible(&self, visible: bool) {
+ let _ = self.proxy.send_event(Visible(visible));
}
/// close window
pub fn close(&self) {
- let _ = self.proxy.send_event(UserWindowEvent::CloseWindow);
+ let _ = self.proxy.send_event(CloseWindow);
}
/// set window to focus
pub fn focus(&self) {
- let _ = self.proxy.send_event(UserWindowEvent::FocusWindow);
+ let _ = self.proxy.send_event(FocusWindow);
+ }
+
+ /// change window to fullscreen
+ pub fn set_fullscreen(&self, fullscreen: bool) {
+ let _ = self.proxy.send_event(Fullscreen(fullscreen));
+ }
+
+ /// set resizable state
+ pub fn set_resizable(&self, resizable: bool) {
+ let _ = self.proxy.send_event(Resizable(resizable));
+ }
+
+ /// set the window always on top
+ pub fn set_always_on_top(&self, top: bool) {
+ let _ = self.proxy.send_event(AlwaysOnTop(top));
+ }
+
+ /// set cursor visible or not
+ pub fn set_cursor_visible(&self, visible: bool) {
+ let _ = self.proxy.send_event(CursorVisible(visible));
+ }
+
+ /// set cursor grab
+ pub fn set_cursor_grab(&self, grab: bool) {
+ let _ = self.proxy.send_event(CursorGrab(grab));
+ }
+
+ /// set window title
+ pub fn set_title(&self, title: &str) {
+ let _ = self.proxy.send_event(SetTitle(String::from(title)));
+ }
+
+ /// change window to borderless
+ pub fn set_decorations(&self, decoration: bool) {
+ let _ = self.proxy.send_event(SetDecorations(decoration));
+ }
+
+ /// opens DevTool window
+ pub fn devtool(&self) {
+ let _ = self.proxy.send_event(DevTool);
}
}
-/// use this function can get the `DesktopContext` context.
-pub fn use_window(cx: &ScopeState) -> &Rc {
- cx.use_hook(|_| cx.consume_context::())
- .as_ref()
- .unwrap()
+use wry::application::event_loop::ControlFlow;
+use wry::application::window::Fullscreen as WryFullscreen;
+
+use crate::controller::DesktopController;
+
+#[derive(Debug)]
+pub enum UserWindowEvent {
+ Update,
+
+ CloseWindow,
+ DragWindow,
+ FocusWindow,
+
+ Visible(bool),
+ Minimize(bool),
+ Maximize(bool),
+ MaximizeToggle,
+ Resizable(bool),
+ AlwaysOnTop(bool),
+ Fullscreen(bool),
+
+ CursorVisible(bool),
+ CursorGrab(bool),
+
+ SetTitle(String),
+ SetDecorations(bool),
+
+ DevTool,
+}
+
+pub(super) fn handler(
+ user_event: UserWindowEvent,
+ desktop: &mut DesktopController,
+ control_flow: &mut ControlFlow,
+) {
+ // currently dioxus-desktop supports a single window only,
+ // so we can grab the only webview from the map;
+ let webview = desktop.webviews.values().next().unwrap();
+ let window = webview.window();
+
+ match user_event {
+ Update => desktop.try_load_ready_webviews(),
+ CloseWindow => *control_flow = ControlFlow::Exit,
+ DragWindow => {
+ // if the drag_window has any errors, we don't do anything
+ window.fullscreen().is_none().then(|| window.drag_window());
+ }
+ Visible(state) => window.set_visible(state),
+ Minimize(state) => window.set_minimized(state),
+ Maximize(state) => window.set_maximized(state),
+ MaximizeToggle => window.set_maximized(!window.is_maximized()),
+ Fullscreen(state) => {
+ if let Some(handle) = window.current_monitor() {
+ window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle))));
+ }
+ }
+ FocusWindow => window.set_focus(),
+ Resizable(state) => window.set_resizable(state),
+ AlwaysOnTop(state) => window.set_always_on_top(state),
+
+ CursorVisible(state) => window.set_cursor_visible(state),
+ CursorGrab(state) => {
+ let _ = window.set_cursor_grab(state);
+ }
+
+ SetTitle(content) => window.set_title(&content),
+ SetDecorations(state) => window.set_decorations(state),
+
+ DevTool => webview.devtool(),
+ }
}
diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs
index 760bf7662..44126dd63 100644
--- a/packages/desktop/src/events.rs
+++ b/packages/desktop/src/events.rs
@@ -1,5 +1,4 @@
-//! Convert a serialized event to an event Trigger
-//!
+//! Convert a serialized event to an event trigger
use std::any::Any;
use std::sync::Arc;
@@ -7,27 +6,49 @@ use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_html::on::*;
+#[derive(serde::Serialize, serde::Deserialize)]
+pub(crate) struct IpcMessage {
+ method: String,
+ params: serde_json::Value,
+}
+
+impl IpcMessage {
+ pub(crate) fn method(&self) -> &str {
+ self.method.as_str()
+ }
+
+ pub(crate) fn params(self) -> serde_json::Value {
+ self.params
+ }
+}
+
+pub(crate) fn parse_ipc_message(payload: &str) -> Option {
+ match serde_json::from_str(payload) {
+ Ok(message) => Some(message),
+ Err(e) => {
+ log::error!("could not parse IPC message, error: {}", e);
+ None
+ }
+ }
+}
+
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
- // scope: u64,
contents: serde_json::Value,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
- let ims: Vec = serde_json::from_value(val).unwrap();
-
let ImEvent {
event,
mounted_dom_id,
contents,
- } = ims.into_iter().next().unwrap();
+ } = serde_json::from_value(val).unwrap();
- // let scope_id = ScopeId(scope as usize);
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
- let name = event_name_from_typ(&event);
+ let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);
UserEvent {
@@ -105,7 +126,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc &'static str {
+fn event_name_from_type(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html
index 88fcba12a..292a1ae6a 100644
--- a/packages/desktop/src/index.html
+++ b/packages/desktop/src/index.html
@@ -1,22 +1,15 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ });
+
+
diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs
index 9181d04dc..4b2615d54 100644
--- a/packages/desktop/src/lib.rs
+++ b/packages/desktop/src/lib.rs
@@ -1,81 +1,31 @@
-//! Dioxus Desktop Renderer
-//!
-//! Render the Dioxus VirtualDom using the platform's native WebView implementation.
-//!
-//! # Desktop
-//!
-//! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
-//!
-//! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
-//!
-//!
-//! ## Getting Set up
-//!
-//! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
-//!
-//! ```shell
-//! $ cargo new --bin demo
-//! $ cd app
-//! ```
-//!
-//! Add Dioxus with the `desktop` feature:
-//!
-//! ```shell
-//! $ cargo add dioxus --features desktop
-//! ```
-//!
-//! Edit your `main.rs`:
-//!
-//! ```rust
-//! // main.rs
-//! use dioxus::prelude::*;
-//!
-//! fn main() {
-//! dioxus::desktop::launch(app);
-//! }
-//!
-//! fn app(cx: Scope) -> Element {
-//! cx.render(rsx!{
-//! div {
-//! "hello world!"
-//! }
-//! })
-//! }
-//! ```
-//!
-//!
-//! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
-//!
-//! ## Future Steps
-//!
-//! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
+#![doc = include_str!("readme.md")]
+#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
+#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
+#![deny(missing_docs)]
-pub mod cfg;
-pub mod desktop_context;
-pub mod escape;
-pub mod events;
+mod cfg;
+mod controller;
+mod desktop_context;
+mod escape;
+mod events;
+mod protocol;
+use desktop_context::UserWindowEvent;
+pub use desktop_context::{use_window, DesktopContext};
+pub use wry;
+pub use wry::application as tao;
+
+use crate::events::trigger_from_serialized;
use cfg::DesktopConfig;
-pub use desktop_context::use_window;
-use desktop_context::DesktopContext;
+use controller::DesktopController;
use dioxus_core::*;
-use std::{
- collections::{HashMap, VecDeque},
- sync::atomic::AtomicBool,
- sync::{Arc, RwLock},
-};
+use events::parse_ipc_message;
use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
- window::{Window, WindowId},
-};
-pub use wry;
-pub use wry::application as tao;
-use wry::{
- application::event_loop::EventLoopProxy,
- webview::RpcRequest,
- webview::{WebView, WebViewBuilder},
+ window::Window,
};
+use wry::webview::WebViewBuilder;
/// Launch the WebView and run the event loop.
///
@@ -152,7 +102,7 @@ pub fn launch_with_props(
props: P,
builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
) {
- let mut cfg = DesktopConfig::default();
+ let mut cfg = DesktopConfig::default().with_default_icon();
builder(&mut cfg);
let event_loop = EventLoop::with_user_event();
@@ -173,29 +123,32 @@ pub fn launch_with_props(
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
let proxy = proxy.clone();
+
let file_handler = cfg.file_drop_handler.take();
+ let resource_dir = cfg.resource_dir.clone();
+
let mut webview = WebViewBuilder::new(window)
.unwrap()
+ .with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
- .with_rpc_handler(move |_window: &Window, req: RpcRequest| {
- match req.method.as_str() {
- "user_event" => {
- let event = events::trigger_from_serialized(req.params.unwrap());
- log::trace!("User event: {:?}", event);
- sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
- }
- "initialize" => {
- is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
- let _ = proxy.send_event(UserWindowEvent::Update);
- }
- "browser_open" => {
- println!("browser_open");
- let data = req.params.unwrap();
- log::trace!("Open browser: {:?}", data);
- if let Some(arr) = data.as_array() {
- if let Some(temp) = arr[0].as_object() {
+ .with_ipc_handler(move |_window: &Window, payload: String| {
+ parse_ipc_message(&payload)
+ .map(|message| match message.method() {
+ "user_event" => {
+ let event = trigger_from_serialized(message.params());
+ log::trace!("User event: {:?}", event);
+ sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
+ }
+ "initialize" => {
+ is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
+ let _ = proxy.send_event(UserWindowEvent::Update);
+ }
+ "browser_open" => {
+ let data = message.params();
+ log::trace!("Open browser: {:?}", data);
+ if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
@@ -204,54 +157,14 @@ pub fn launch_with_props(
}
}
}
- }
- _ => {}
- }
- None
+ _ => (),
+ })
+ .unwrap_or_else(|| {
+ log::warn!("invalid IPC message received");
+ });
})
- .with_custom_protocol(String::from("dioxus"), move |request| {
- // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case"
- // For now, we only serve two pieces of content which get included as bytes into the final binary.
- let path = request.uri().replace("dioxus://", "");
-
- // all assets shouldbe called from index.html
- let trimmed = path.trim_start_matches("index.html/");
-
- if trimmed.is_empty() {
- wry::http::ResponseBuilder::new()
- .mimetype("text/html")
- .body(include_bytes!("./index.html").to_vec())
- } else if trimmed == "index.js" {
- wry::http::ResponseBuilder::new()
- .mimetype("text/javascript")
- .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec())
- } else {
- // Read the file content from file path
- use std::fs::read;
-
- let path_buf = std::path::Path::new(trimmed).canonicalize()?;
- let cur_path = std::path::Path::new(".").canonicalize()?;
-
- if !path_buf.starts_with(cur_path) {
- return wry::http::ResponseBuilder::new()
- .status(wry::http::status::StatusCode::FORBIDDEN)
- .body(String::from("Forbidden").into_bytes());
- }
-
- if !path_buf.exists() {
- return wry::http::ResponseBuilder::new()
- .status(wry::http::status::StatusCode::NOT_FOUND)
- .body(String::from("Not Found").into_bytes());
- }
-
- let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
-
- // do not let path searching to go two layers beyond the caller level
- let data = read(path_buf)?;
- let meta = format!("{}", mime);
-
- wry::http::ResponseBuilder::new().mimetype(&meta).body(data)
- }
+ .with_custom_protocol(String::from("dioxus"), move |r| {
+ protocol::desktop_handler(r, resource_dir.clone())
})
.with_file_drop_handler(move |window, evet| {
file_handler
@@ -260,10 +173,32 @@ pub fn launch_with_props(
.unwrap_or_default()
});
- for (name, handler) in cfg.protocos.drain(..) {
+ for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
+ if cfg.disable_context_menu {
+ // in release mode, we don't want to show the dev tool or reload menus
+ webview = webview.with_initialization_script(
+ r#"
+ if (document.addEventListener) {
+ document.addEventListener('contextmenu', function(e) {
+ alert("You've tried to open context menu");
+ e.preventDefault();
+ }, false);
+ } else {
+ document.attachEvent('oncontextmenu', function() {
+ alert("You've tried to open context menu");
+ window.event.returnValue = false;
+ });
+ }
+ "#,
+ )
+ } else {
+ // in debug, we are okay with the reload menu showing and dev tool
+ webview = webview.with_dev_tool(true);
+ }
+
desktop.webviews.insert(window_id, webview.build().unwrap());
}
@@ -282,46 +217,8 @@ pub fn launch_with_props(
_ => {}
},
- Event::UserEvent(_evt) => {
- //
- match _evt {
- UserWindowEvent::Update => desktop.try_load_ready_webviews(),
- UserWindowEvent::DragWindow => {
- // this loop just run once, because dioxus-desktop is unsupport multi-window.
- for webview in desktop.webviews.values() {
- let window = webview.window();
- // start to drag the window.
- // if the drag_window have any err. we don't do anything.
- let _ = window.drag_window();
- }
- }
- UserWindowEvent::CloseWindow => {
- // close window
- *control_flow = ControlFlow::Exit;
- }
- UserWindowEvent::Minimize(state) => {
- // this loop just run once, because dioxus-desktop is unsupport multi-window.
- for webview in desktop.webviews.values() {
- let window = webview.window();
- // change window minimized state.
- window.set_minimized(state);
- }
- }
- UserWindowEvent::Maximize(state) => {
- // this loop just run once, because dioxus-desktop is unsupport multi-window.
- for webview in desktop.webviews.values() {
- let window = webview.window();
- // change window maximized state.
- window.set_maximized(state);
- }
- }
- UserWindowEvent::FocusWindow => {
- for webview in desktop.webviews.values() {
- let window = webview.window();
- window.set_focus();
- }
- }
- }
+ Event::UserEvent(user_event) => {
+ desktop_context::handler(user_event, &mut desktop, control_flow)
}
Event::MainEventsCleared => {}
Event::Resumed => {}
@@ -332,108 +229,3 @@ pub fn launch_with_props(
}
})
}
-
-pub enum UserWindowEvent {
- Update,
- DragWindow,
- CloseWindow,
- FocusWindow,
- Minimize(bool),
- Maximize(bool),
-}
-
-pub struct DesktopController {
- pub proxy: EventLoopProxy,
- pub webviews: HashMap,
- pub sender: futures_channel::mpsc::UnboundedSender,
- pub pending_edits: Arc>>,
- pub quit_app_on_close: bool,
- pub is_ready: Arc,
-}
-
-impl DesktopController {
- // Launch the virtualdom on its own thread managed by tokio
- // returns the desktop state
- pub fn new_on_tokio(
- root: Component,
- props: P,
- evt: EventLoopProxy,
- ) -> Self {
- let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
- let pending_edits = edit_queue.clone();
-
- let (sender, receiver) = futures_channel::mpsc::unbounded::();
- let return_sender = sender.clone();
- let proxy = evt.clone();
-
- let desktop_context_proxy = proxy.clone();
- std::thread::spawn(move || {
- // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
- let runtime = tokio::runtime::Builder::new_multi_thread()
- .enable_all()
- .build()
- .unwrap();
-
- runtime.block_on(async move {
- let mut dom =
- VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
-
- let window_context = DesktopContext::new(desktop_context_proxy);
-
- dom.base_scope().provide_context(window_context);
-
- let edits = dom.rebuild();
-
- edit_queue
- .write()
- .unwrap()
- .push_front(serde_json::to_string(&edits.edits).unwrap());
-
- loop {
- dom.wait_for_work().await;
- let mut muts = dom.work_with_deadline(|| false);
-
- while let Some(edit) = muts.pop() {
- edit_queue
- .write()
- .unwrap()
- .push_front(serde_json::to_string(&edit.edits).unwrap());
- }
-
- let _ = evt.send_event(UserWindowEvent::Update);
- }
- })
- });
-
- Self {
- pending_edits,
- sender: return_sender,
- proxy,
- webviews: HashMap::new(),
- is_ready: Arc::new(AtomicBool::new(false)),
- quit_app_on_close: true,
- }
- }
-
- pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
- self.webviews.remove(&window_id);
-
- if self.webviews.is_empty() && self.quit_app_on_close {
- *control_flow = ControlFlow::Exit;
- }
- }
-
- pub fn try_load_ready_webviews(&mut self) {
- if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
- let mut queue = self.pending_edits.write().unwrap();
- let (_id, view) = self.webviews.iter_mut().next().unwrap();
-
- while let Some(edit) = queue.pop_back() {
- view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
- .unwrap();
- }
- } else {
- println!("waiting for ready");
- }
- }
-}
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
new file mode 100644
index 000000000..bd8bb3d84
--- /dev/null
+++ b/packages/desktop/src/protocol.rs
@@ -0,0 +1,83 @@
+use std::path::{Path, PathBuf};
+use wry::{
+ http::{status::StatusCode, Request, Response, ResponseBuilder},
+ Result,
+};
+
+pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> Result {
+ // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
+ // For now, we only serve two pieces of content which get included as bytes into the final binary.
+ let path = request.uri().replace("dioxus://", "");
+
+ // all assets should be called from index.html
+ let trimmed = path.trim_start_matches("index.html/");
+
+ if trimmed.is_empty() {
+ ResponseBuilder::new()
+ .mimetype("text/html")
+ .body(include_bytes!("./index.html").to_vec())
+ } else if trimmed == "index.js" {
+ ResponseBuilder::new()
+ .mimetype("text/javascript")
+ .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
+ } else {
+ let asset_root = asset_root
+ .unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()))
+ .canonicalize()?;
+
+ let asset = asset_root.join(trimmed).canonicalize()?;
+
+ if !asset.starts_with(asset_root) {
+ return ResponseBuilder::new()
+ .status(StatusCode::FORBIDDEN)
+ .body(String::from("Forbidden").into_bytes());
+ }
+
+ if !asset.exists() {
+ return ResponseBuilder::new()
+ .status(StatusCode::NOT_FOUND)
+ .body(String::from("Not Found").into_bytes());
+ }
+
+ let mime = mime_guess::from_path(&asset).first_or_octet_stream();
+
+ // do not let path searching to go two layers beyond the caller level
+ let data = std::fs::read(asset)?;
+ let meta = format!("{}", mime);
+
+ ResponseBuilder::new().mimetype(&meta).body(data)
+ }
+}
+
+#[allow(unreachable_code)]
+fn get_asset_root() -> Option {
+ /*
+ We're matching exactly how cargo-bundle works.
+
+ - [x] macOS
+ - [ ] Windows
+ - [ ] Linux (rpm)
+ - [ ] Linux (deb)
+ - [ ] iOS
+ - [ ] Android
+
+ */
+
+ if std::env::var_os("CARGO").is_some() {
+ return None;
+ }
+
+ // TODO: support for other platforms
+ #[cfg(target_os = "macos")]
+ {
+ let bundle = core_foundation::bundle::CFBundle::main_bundle();
+ let bundle_path = dbg!(bundle.path()?);
+ let resources_path = dbg!(bundle.resources_path()?);
+ let absolute_resources_root = dbg!(bundle_path.join(resources_path));
+ let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
+
+ return Some(canonical_resources_root);
+ }
+
+ None
+}
diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md
new file mode 100644
index 000000000..dc034fe67
--- /dev/null
+++ b/packages/desktop/src/readme.md
@@ -0,0 +1,51 @@
+Dioxus Desktop Renderer
+
+Render the Dioxus VirtualDom using the platform's native WebView implementation.
+
+# Desktop
+
+One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
+
+Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
+
+
+## Getting Set up
+
+Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
+
+```shell
+$ cargo new --bin demo
+$ cd app
+```
+
+Add Dioxus with the `desktop` feature:
+
+```shell
+$ cargo add dioxus --features desktop
+```
+
+Edit your `main.rs`:
+
+```rust
+// main.rs
+use dioxus::prelude::*;
+
+fn main() {
+ dioxus::desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ cx.render(rsx!{
+ div {
+ "hello world!"
+ }
+ })
+}
+```
+
+
+To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
+
+## Future Steps
+
+Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
\ No newline at end of file
diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml
new file mode 100644
index 000000000..48fe377c7
--- /dev/null
+++ b/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"
diff --git a/packages/fermi/README.md b/packages/fermi/README.md
new file mode 100644
index 000000000..0923aa6f6
--- /dev/null
+++ b/packages/fermi/README.md
@@ -0,0 +1,92 @@
+
+
+
Fermi ⚛
+
+ Atom-based global state management solution for Dioxus
+
+
+
+
+
+
+-----
+
+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
diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs
new file mode 100644
index 000000000..2b99174b5
--- /dev/null
+++ b/packages/fermi/src/atoms/atom.rs
@@ -0,0 +1,28 @@
+use crate::{AtomId, AtomRoot, Readable, Writable};
+
+pub type Atom = fn(AtomBuilder) -> T;
+pub struct AtomBuilder;
+
+impl Readable for Atom {
+ fn read(&self, _root: AtomRoot) -> Option {
+ todo!()
+ }
+ fn init(&self) -> V {
+ (*self)(AtomBuilder)
+ }
+ fn unique_id(&self) -> AtomId {
+ *self as *const ()
+ }
+}
+
+impl Writable for Atom {
+ fn write(&self, _root: AtomRoot, _value: V) {
+ todo!()
+ }
+}
+
+#[test]
+fn atom_compiles() {
+ static TEST_ATOM: Atom<&str> = |_| "hello";
+ dbg!(TEST_ATOM.init());
+}
diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs
new file mode 100644
index 000000000..60fc22bcb
--- /dev/null
+++ b/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 = fn(AtomFamilyBuilder) -> ImMap;
+
+impl Readable> for AtomFamily {
+ fn read(&self, _root: AtomRoot) -> Option> {
+ todo!()
+ }
+
+ fn init(&self) -> ImMap {
+ (*self)(AtomFamilyBuilder)
+ }
+
+ fn unique_id(&self) -> AtomId {
+ *self as *const ()
+ }
+}
+
+impl Writable> for AtomFamily {
+ fn write(&self, _root: AtomRoot, _value: ImMap) {
+ todo!()
+ }
+}
diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs
new file mode 100644
index 000000000..59e412031
--- /dev/null
+++ b/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 = fn(AtomRefBuilder) -> T;
+
+impl Readable> for AtomRef {
+ fn read(&self, _root: AtomRoot) -> Option> {
+ todo!()
+ }
+
+ fn init(&self) -> RefCell {
+ RefCell::new((*self)(AtomRefBuilder))
+ }
+
+ fn unique_id(&self) -> AtomId {
+ *self as *const ()
+ }
+}
+
+#[test]
+fn atom_compiles() {
+ static TEST_ATOM: AtomRef> = |_| vec![];
+ dbg!(TEST_ATOM.init());
+}
diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/packages/fermi/src/atoms/selector.rs
@@ -0,0 +1 @@
+
diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/packages/fermi/src/atoms/selectorfamily.rs
@@ -0,0 +1 @@
+
diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs
new file mode 100644
index 000000000..a7ca9155f
--- /dev/null
+++ b/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,
+}
+
+impl CallbackApi {
+ // get the current value of the atom
+ pub fn get(&self, atom: impl Readable) -> &V {
+ todo!()
+ }
+
+ // get the current value of the atom in its RC container
+ pub fn get_rc(&self, atom: impl Readable) -> &Rc {
+ todo!()
+ }
+
+ // set the current value of the atom
+ pub fn set(&self, atom: impl Writable, 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();
+ )*}
+}
diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs
new file mode 100644
index 000000000..da9ad9367
--- /dev/null
+++ b/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(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef {
+ 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 {
+ ptr: AtomId,
+ value: Rc>,
+ root: Rc,
+ scope_id: ScopeId,
+}
+
+impl UseAtomRef {
+ pub fn read(&self) -> Ref {
+ self.value.borrow()
+ }
+
+ pub fn write(&self) -> RefMut {
+ self.root.force_update(self.ptr);
+ self.value.borrow_mut()
+ }
+
+ pub fn write_silent(&self) -> RefMut {
+ 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 Drop for UseAtomRef {
+ fn drop(&mut self) {
+ self.root.unsubscribe(self.ptr, self.scope_id)
+ }
+}
diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs
new file mode 100644
index 000000000..d2945025b
--- /dev/null
+++ b/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 {
+ cx.use_hook(|_| match cx.consume_context::() {
+ Some(root) => root,
+ None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())),
+ })
+}
diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs
new file mode 100644
index 000000000..edb8aafb8
--- /dev/null
+++ b/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 {
+ cx.use_hook(|_| match cx.consume_context::