From c1fd848f89b0146581d8e485fa0d4a847387b963 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 31 May 2021 18:55:56 -0400 Subject: [PATCH] WIP: move to static props --- .vscode/spellright.dict | 8 + Cargo.toml | 3 +- notes/ARCHITECTURE.md | 18 -- notes/CHANGELOG.md | 53 +++- notes/ROADMAP.md | 50 +++- packages/core-macro/src/htm.rs | 47 +-- packages/core-macro/src/lib.rs | 23 +- packages/core-macro/src/rsxt.rs | 31 +- packages/core-macro/src/rsxtemplate.rs | 72 +++++ packages/core-macro/src/util.rs | 26 +- packages/core/examples/borrowed.rs | 37 ++- packages/core/examples/component_child.rs | 68 +++++ packages/core/examples/template.rs | 8 + packages/core/examples/text.rs | 16 + packages/core/src/component.rs | 2 +- packages/core/src/nodebuilder.rs | 39 ++- packages/core/src/nodes.rs | 8 +- packages/core/src/virtual_dom.rs | 130 ++++---- packages/liveview/index.html | 6 +- packages/recoil/Cargo.toml | 8 +- packages/recoil/README.md | 109 ++++++- packages/recoil/examples/family.rs | 31 +- packages/recoil/examples/familypoc.rs | 47 +++ .../{helloworld.rs => hellorecoil.rs} | 13 +- packages/recoil/examples/selectorlist.rs | 4 +- packages/recoil/examples/test.html | 1 + packages/recoil/notes/Architecture.md | 4 +- packages/recoil/src/lib.rs | 277 +++++++++++------- packages/recoil/src/tracingimmap.rs | 29 ++ packages/web/examples/hello.rs | 15 +- packages/web/examples/htmlexample.html | 59 ++++ packages/web/examples/list.rs | 1 - packages/web/examples/todomvc_simple.rs | 3 +- packages/web/examples/todomvcsingle.rs | 22 +- packages/web/src/interpreter.rs | 18 +- packages/webview/Cargo.toml | 2 +- packages/webview/examples/demo.rs | 67 +++-- 37 files changed, 1015 insertions(+), 340 deletions(-) create mode 100644 packages/core-macro/src/rsxtemplate.rs create mode 100644 packages/core/examples/component_child.rs create mode 100644 packages/core/examples/template.rs create mode 100644 packages/core/examples/text.rs create mode 100644 packages/recoil/examples/familypoc.rs rename packages/recoil/examples/{helloworld.rs => hellorecoil.rs} (60%) create mode 100644 packages/recoil/examples/test.html create mode 100644 packages/recoil/src/tracingimmap.rs create mode 100644 packages/web/examples/htmlexample.html diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 2658c42e..7730a4f6 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -35,3 +35,11 @@ Gloo mobx img svg +Actix +OrdMap +datastructures +onclick +virtualdom +namespaced +namespacing +impl diff --git a/Cargo.toml b/Cargo.toml index a990ab17..62447338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,8 @@ members = [ "packages/recoil", "packages/docsite", "packages/ssr", -] # "packages/webview", + "packages/webview", +] # "packages/cli", # "packages/webview", diff --git a/notes/ARCHITECTURE.md b/notes/ARCHITECTURE.md index cd0140e5..e69de29b 100644 --- a/notes/ARCHITECTURE.md +++ b/notes/ARCHITECTURE.md @@ -1,18 +0,0 @@ -# Dioxus Architecture - -:) - - -```rust - -let data = use_context(); -data.set(abc); - -unsafe { - // data is unsafely aliased - data.modify(|&mut data| { - - }) -} - -``` diff --git a/notes/CHANGELOG.md b/notes/CHANGELOG.md index b6d3ceec..26d3dd6e 100644 --- a/notes/CHANGELOG.md +++ b/notes/CHANGELOG.md @@ -1,5 +1,7 @@ # Dioxus v0.1.0 + Welcome to the first iteration of the Dioxus Virtual DOM! This release brings support for: + - Web via WASM - Desktop via webview integration - Server-rendering with custom ToString implementation @@ -9,27 +11,35 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - Context API - Basic suspense - Controlled components ----- + +--- + ## Project: Initial VDOM support (TBD) + > Get the initial VDom + Event System + Patching + Diffing + Component framework up and running > Get a demo working using just the web + - [x] (Core) Migrate virtual node into new VNode type - [x] (Core) Arena allocate VNodes - [x] (Core) Allow VNodes to borrow arena contents - [x] (Core) Introduce the VDOM and patch API for 3rd party renderers - [x] (Core) Implement lifecycle -- [x] (Core) Implement an event system +- [x] (Core) Implement an event system - [x] (Core) Implement child nodes, scope creation - [x] (Core) Implement dirty tagging and compression -## Project: QOL +## Project: QOL + > Make it easier to write components + - [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed/alloced) -- [x] (Macro) Tweak component syntax to accept a new custom element +- [x] (Macro) Tweak component syntax to accept a new custom element - [ ] (Macro) Allow components to specify their props as function args ## Project: Hooks + Context + Subscriptions (TBD) + > Implement the foundations for state management + - [x] Implement context object - [x] Implement use_state (rewrite to use the use_reducer api like rei) - [x] Implement use_ref @@ -37,54 +47,66 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [ ] Implement use_reducer (WIP) ## Project: String Render (TBD) -> Implement a light-weight string renderer with basic caching + +> Implement a light-weight string renderer with basic caching + - [x] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" - [x] (SSR) Implement stateful 3rd party string renderer ## Project: Web_sys renderer (TBD) + - [x] WebSys edit interpreter - [x] Event system using async channels - [ ] Implement conversion of all event types into synthetic events ## Project: Web-View 🤲 🍨 + > Proof of concept: stream render edits from server to client + - [x] Prove that the diffing and patching framework can support patch streaming ## Project: Examples -> Get *all* the examples + +> Get _all_ the examples + - [ ] (Examples) Tide example with templating -## Project: State management +## Project: State management + > Get some global state management installed with the hooks + context API - ## Project: Concurrency (TBD) -> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted -? +> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted +> ? ## Project: Mobile exploration - ## Project: Live-View 🤲 🍨 -> Combine the server and client into a single file :) +> Combine the server and client into a single file :) ## Project: Sanitization (TBD) + > Improve code health + - [ ] (Macro) Clippy sanity for html macro - [ ] (Macro) Error sanitization - ## Outstanding todos: + > anything missed so far + - [x] keys on components - [x] Allow paths for components - [x] todo mvc - [x] Tweak macro parsing for better errors - [x] dirty tagging, compression - [x] code health -- [ ] name spacing so svg works +- [x] static str slice optimization +- [x] name spacing so svg works + - [x] A handful of svg elements are automatically namespaced + - [ ] Allow hierarchical namespacing (all children share a parent's namespace) - TBD in macro impl - [ ] fix keys on elements - [ ] controlled components (kinda tuff since we need all these different platforms) - [ ] Their own crate @@ -93,6 +115,8 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [ ] Re-exported through the `dioxus` crate (not essential to core virtualdom) ## Less-essential todos + +- [ ] HTML doesn't require strings between elements (copy-paste from internet) - [ ] Make events lazy (use traits + Box) - not sure what this means anymore - [ ] Beef up the dioxus CLI tool to report build progress - [ ] Extract arena logic out for better safety guarantees @@ -105,6 +129,7 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [ ] Website lower priority features + - [ ] Attributes on elements should implement format_args instead of string fmt - [ ] fragments - [ ] node refs (postpone for future release?) diff --git a/notes/ROADMAP.md b/notes/ROADMAP.md index 7e54d653..cdee47e6 100644 --- a/notes/ROADMAP.md +++ b/notes/ROADMAP.md @@ -1,70 +1,90 @@ # Road map + This release map gives a sense of the release cadence and features per update going forward into the future. PRs are required to be squashed before merging. For each point release, we save a branch on master (0.1, 0.2, 0.3, master). Eventually, we'll remove these in favor of higher point releases when dioxus is stabilized. Any live PRs will be merged into the dev branch. -Until 0.3, Dioxus will be in stealth mode. The goal is to launch with a bountiful feature set and a cohesive API before OSS tears it apart :). Once LiveView is ready, then Dioxus will launch completely with a beta service for LiveHost. +Until 0.3, Dioxus will be in stealth mode. The goal is to launch with a bountiful feature set and a cohesive API before OSS tears it apart :). ## v0.1: Bare Necessities + > Enable ergonomic and performant webapps + --- + Dioxus Core + - Lifecycles for components - Internal event system - Diffing - Patching Html macro + - special formatting - closure handlers - child handlers - iterator handlers - + Dioxus web + - a - -Dioxus CLI + +Dioxus CLI + - Develop - Bundle - Test - + Server-side-rendering + - Write nodes to string - Integration with tide, Actix, warp Dioxus WebView (desktop) + - One-file setup for desktop apps - Integration with the web browser for rapid development ## v0.2: Bread and butter + > Complex apps? CHECK + --- + State management + - Dioxus-Reducer as the blessed redux alternative - Includes thunks and reducers (async dispatches) -- Dioxus-Dataflow as the blessed recoil alternative +- Dioxus-DataFlow as the blessed recoil alternative - The hip, new approach for granular state Dioxus CLI + - Visual tool? - Asset bundling service - -Dioxus DevTools integration with the web -- Basic support for pure liveview/webview +Dioxus DevTools integration with the web + +- Basic support for pure liveview/webview ## v0.3: Superpowers + > Enable LiveView for fullstack development + --- + Dioxus LiveView - - Custom server built on Actix (or something fast) - - Ergonomic builders - - Concurrent system built into dioxus core + +- Custom server built on Actix (or something fast) +- Ergonomic builders +- Concurrent system built into dioxus core Dioxus iOS - - Initial support via webview - - Look into native support based on how Flutter/SwiftUI works + +- Initial support via webview +- Look into native support based on how Flutter/SwiftUI works Dioxus Android +## v0.4: Community -## v0.4: Community > Foster the incoming community diff --git a/packages/core-macro/src/htm.rs b/packages/core-macro/src/htm.rs index a61f2494..143c50cf 100644 --- a/packages/core-macro/src/htm.rs +++ b/packages/core-macro/src/htm.rs @@ -13,6 +13,8 @@ //! //! +use crate::util::is_valid_svg_tag; + use { proc_macro::TokenStream, proc_macro2::{Span, TokenStream as TokenStream2}, @@ -32,13 +34,17 @@ pub struct HtmlRender { } impl Parse for HtmlRender { - fn parse(s: ParseStream) -> Result { + fn parse(input: ParseStream) -> Result { + if input.peek(LitStr) { + return input.parse::()?.parse::(); + } + // let ctx: 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 kind = if input.peek(token::Bracket) { let nodes_toks; - syn::bracketed!(nodes_toks in s); + syn::bracketed!(nodes_toks in input); let mut nodes: Vec> = vec![nodes_toks.parse()?]; while nodes_toks.peek(Token![,]) { nodes_toks.parse::()?; @@ -46,7 +52,7 @@ impl Parse for HtmlRender { } NodeOrList::List(NodeList(nodes)) } else { - NodeOrList::Node(s.parse()?) + NodeOrList::Node(input.parse()?) }; Ok(HtmlRender { kind }) } @@ -147,13 +153,20 @@ struct Element { impl ToTokens for ToToksCtx<&Element> { fn to_tokens(&self, tokens: &mut TokenStream2) { // let ctx = self.ctx; - let _name = &self.inner.name; + let name = &self.inner.name.to_string(); tokens.append_all(quote! { - dioxus::builder::ElementBuilder::new(ctx, "#name") + dioxus::builder::ElementBuilder::new(ctx, #name) }); for attr in self.inner.attrs.iter() { self.recurse(attr).to_tokens(tokens); } + + if is_valid_svg_tag(name) { + tokens.append_all(quote! { + .namespace(Some("http://www.w3.org/2000/svg")) + }); + } + match &self.inner.children { MaybeExpr::Expr(expr) => tokens.append_all(quote! { .children(#expr) @@ -229,6 +242,7 @@ impl Parse for Element { )); } s.parse::]>()?; + Ok(Self { name, attrs, @@ -297,9 +311,15 @@ impl ToTokens for ToToksCtx<&Attr> { match &self.inner.ty { AttrType::Value(value) => { let value = self.recurse(value); - tokens.append_all(quote! { - .attr(#name, #value) - }); + if name == "xmlns" { + tokens.append_all(quote! { + .namespace(Some(#value)) + }); + } else { + tokens.append_all(quote! { + .attr(#name, format_args_f!(#value)) + }); + } } AttrType::Event(event) => { tokens.append_all(quote! { @@ -334,14 +354,7 @@ impl ToTokens for ToToksCtx<&TextNode> { fn to_tokens(&self, tokens: &mut TokenStream2) { let mut token_stream = TokenStream2::new(); self.recurse(&self.inner.0).to_tokens(&mut token_stream); - tokens.append_all(quote! { - { - use bumpalo::core_alloc::fmt::Write; - let mut s = bumpalo::collections::String::new_in(bump); - s.write_fmt(format_args_f!(#token_stream)).unwrap(); - dioxus::builder::text2(s) - } - }); + tokens.append_all(quote! {dioxus::builder::text3(bump, format_args_f!(#token_stream))}); } } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 54691a58..297c8e33 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -2,12 +2,13 @@ use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -mod fc; -mod htm; -mod ifmt; -mod props; -mod rsxt; -mod util; +pub(crate) mod fc; +pub(crate) mod htm; +pub(crate) mod ifmt; +pub(crate) mod props; +pub(crate) mod rsxt; +pub(crate) mod rsxtemplate; +pub(crate) mod util; /// The html! macro makes it easy for developers to write jsx-style markup in their components. /// We aim to keep functional parity with html templates. @@ -29,6 +30,16 @@ pub fn rsx(s: TokenStream) -> TokenStream { } } +/// The html! macro makes it easy for developers to write jsx-style markup in their components. +/// We aim to keep functional parity with html templates. +#[proc_macro] +pub fn rsx_template(s: TokenStream) -> TokenStream { + match syn::parse::(s) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } +} + // #[proc_macro_attribute] // pub fn fc(attr: TokenStream, item: TokenStream) -> TokenStream { diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index 7168f091..7707c772 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -1,6 +1,6 @@ use syn::parse::{discouraged::Speculative, ParseBuffer}; -use crate::util::is_valid_html_tag; +use crate::util::is_valid_tag; use { proc_macro::TokenStream, @@ -23,12 +23,16 @@ pub struct RsxRender { impl Parse for RsxRender { fn parse(input: ParseStream) -> Result { + if input.peek(LitStr) { + return input.parse::()?.parse::(); + } + // try to parse the first ident and comma let custom_context = if input.peek(Token![in]) && input.peek2(Ident) && input.peek3(Token![,]) { let _ = input.parse::()?; let name = input.parse::()?; - if is_valid_html_tag(&name.to_string()) { + if is_valid_tag(&name.to_string()) { return Err(Error::new( input.span(), "Custom context cannot be an html element name", @@ -114,7 +118,7 @@ impl Parse for AmbiguousElement { if let Ok(name) = input.fork().parse::() { let name_str = name.to_string(); - match is_valid_html_tag(&name_str) { + match is_valid_tag(&name_str) { true => input .parse::() .map(|c| AmbiguousElement::Element(c)), @@ -134,6 +138,9 @@ impl Parse for AmbiguousElement { } } } else { + if input.peek(LitStr) { + panic!("it's actually a litstr"); + } Err(Error::new(input.span(), "Not a valid Html tag")) } } @@ -318,7 +325,7 @@ impl Parse for Element { // let name = Ident::parse(stream)?; - if !crate::util::is_valid_html_tag(&name.to_string()) { + if !crate::util::is_valid_tag(&name.to_string()) { return Err(Error::new(name.span(), "Not a valid Html tag")); } @@ -495,12 +502,7 @@ impl ToTokens for &ElementAttr { match &self.ty { AttrType::BumpText(value) => { tokens.append_all(quote! { - .attr(#name, { - use bumpalo::core_alloc::fmt::Write; - let mut s = bumpalo::collections::String::new_in(bump); - s.write_fmt(format_args_f!(#value)).unwrap(); - s.into_bump_str() - }) + .attr(#name, format_args_f!(#value)) }); } AttrType::Event(event) => { @@ -540,10 +542,11 @@ impl ToTokens for TextNode { let token_stream = &self.0.to_token_stream(); tokens.append_all(quote! { { - use bumpalo::core_alloc::fmt::Write; - let mut s = bumpalo::collections::String::new_in(bump); - s.write_fmt(format_args_f!(#token_stream)).unwrap(); - dioxus::builder::text2(s) + // use bumpalo::core_alloc::fmt::Write; + // let mut s = bumpalo::collections::String::new_in(bump); + // s.write_fmt(format_args_f!(#token_stream)).unwrap(); + dioxus::builder::text3(bump, format_args_f!(#token_stream)) + // dioxus::builder::text2(s) } }); } diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/core-macro/src/rsxtemplate.rs new file mode 100644 index 00000000..7554c418 --- /dev/null +++ b/packages/core-macro/src/rsxtemplate.rs @@ -0,0 +1,72 @@ +use crate::{rsxt::RsxRender, 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: RsxRender, +} + +impl Parse for RsxTemplate { + fn parse(s: ParseStream) -> Result { + if s.peek(LitStr) { + use std::str::FromStr; + let lit = s.parse::()?; + 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 ctx: 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 |ctx| { + // let bump = &ctx.bump(); + + // #new_toks + // }) + // }; + + // final_tokens.to_tokens(out_tokens); + } +} diff --git a/packages/core-macro/src/util.rs b/packages/core-macro/src/util.rs index df321d31..4ef4fdb9 100644 --- a/packages/core-macro/src/util.rs +++ b/packages/core-macro/src/util.rs @@ -2,7 +2,8 @@ use once_cell::sync::Lazy; use std::collections::hash_set::HashSet; -static VALID_TAGS: Lazy> = Lazy::new(|| { +/// rsx! and html! macros support the html namespace as well as svg namespace +static HTML_TAGS: Lazy> = Lazy::new(|| { [ "a", "abbr", @@ -117,9 +118,16 @@ static VALID_TAGS: Lazy> = Lazy::new(|| { "var", "video", "wbr", + ] + .iter() + .cloned() + .collect() +}); + +static SVG_TAGS: Lazy> = Lazy::new(|| { + [ // SVTG - "svg", - "path", + "svg", "path", "g", ] .iter() .cloned() @@ -135,6 +143,14 @@ static VALID_TAGS: Lazy> = Lazy::new(|| { /// /// assert_eq!(is_valid_tag("random"), false); /// ``` -pub fn is_valid_html_tag(tag: &str) -> bool { - VALID_TAGS.contains(tag) +pub fn is_valid_tag(tag: &str) -> bool { + is_valid_html_tag(tag) || is_valid_svg_tag(tag) +} + +pub fn is_valid_html_tag(tag: &str) -> bool { + HTML_TAGS.contains(tag) +} + +pub fn is_valid_svg_tag(tag: &str) -> bool { + SVG_TAGS.contains(tag) } diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index dd85d267..c268a629 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -7,12 +7,12 @@ fn main() {} -use std::{borrow::Borrow, rc::Rc}; +use std::{borrow::Borrow, ops::Deref, rc::Rc}; use dioxus_core::prelude::*; struct Props { - items: Vec, + items: Vec>, } #[derive(PartialEq)] @@ -35,8 +35,8 @@ fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { ChildItem, // create the props with nothing but the fc fc_to_builder(ChildItem) - .item(child) - .item_handler(set_val.clone()) + .item(child.clone()) + .item_handler(Callback(set_val.clone())) .build(), None, )); @@ -46,19 +46,13 @@ fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { } // props should derive a partialeq implementation automatically, but implement ptr compare for & fields -#[derive(Props)] -struct ChildProps<'a> { +#[derive(Props, PartialEq)] +struct ChildProps { // Pass down complex structs - item: &'a ListItem, + item: Rc, // Even pass down handlers! - item_handler: Rc, -} - -impl PartialEq for ChildProps<'_> { - fn eq(&self, _other: &Self) -> bool { - false - } + item_handler: Callback, } fn ChildItem<'a>(ctx: Context<'a>, props: &ChildProps) -> DomTree { @@ -75,3 +69,18 @@ fn ChildItem<'a>(ctx: Context<'a>, props: &ChildProps) -> DomTree { } }) } + +#[derive(Clone)] +struct Callback(Rc O>); +impl Deref for Callback { + type Target = Rc O>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl PartialEq for Callback { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} diff --git a/packages/core/examples/component_child.rs b/packages/core/examples/component_child.rs new file mode 100644 index 00000000..f4170d4f --- /dev/null +++ b/packages/core/examples/component_child.rs @@ -0,0 +1,68 @@ +use std::{ops::Deref, rc::Rc}; + +use dioxus::virtual_dom::Scope; +use dioxus_core::prelude::*; + +type RcStr = Rc; + +fn main() { + let r: RcStr = "asdasd".into(); + let r: RcStr = String::from("asdasd").into(); + + let g = rsx! { + div { + Example {} + } + }; +} + +static Example: FC<()> = |ctx, props| { + let nodes = ctx.children(); + + // + rsx! { in ctx, + div { + {nodes} + } + } +}; + +#[derive(Clone, Copy)] +struct MyContext<'a, T> { + props: &'a T, + inner: &'a Scope, +} +impl<'a, T> MyContext<'a, T> { + fn children(&self) -> Vec> { + todo!() + } + pub fn render2 FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>( + &self, + lazy_nodes: LazyNodes<'a, F>, + ) -> VNode<'a> { + self.inner.render2(lazy_nodes) + } +} + +impl<'a, T> Deref for MyContext<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.props + } +} + +struct MyProps { + title: String, +} + +fn example(scope: MyContext) -> VNode { + let childs = scope.children(); + + scope.inner.render2(rsx! { + div { + "{scope.title}" + {childs} + } + }) +} diff --git a/packages/core/examples/template.rs b/packages/core/examples/template.rs new file mode 100644 index 00000000..eaac05f0 --- /dev/null +++ b/packages/core/examples/template.rs @@ -0,0 +1,8 @@ +use dioxus_core::prelude::*; +use dioxus_core_macro::rsx_template; + +fn main() { + let g = html!("
"); + let g = html!("
"); + // let g = rsx!("div { div { } } "); +} diff --git a/packages/core/examples/text.rs b/packages/core/examples/text.rs new file mode 100644 index 00000000..39d56f51 --- /dev/null +++ b/packages/core/examples/text.rs @@ -0,0 +1,16 @@ +use dioxus_core::prelude::*; +use dioxus_core_macro::format_args_f; + +fn main() { + let num = 123; + let b = Bump::new(); + + let g = rsx! { + div { + "abc {num}" + div { + "asd" + } + } + }; +} diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index b955502b..6a1a49df 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -9,7 +9,7 @@ use crate::innerlude::FC; pub type ScopeIdx = generational_arena::Index; -pub trait Properties: PartialEq { +pub trait Properties: PartialEq + 'static { type Builder; fn builder() -> Self::Builder; } diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 491e655f..eb3361c4 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -387,8 +387,17 @@ where /// // Create the `
` element. /// let my_div = div(&b).attr("id", "my-div").finish(); /// ``` - #[inline] - pub fn attr(mut self, name: &'static str, value: &'a str) -> Self { + pub fn attr(mut self, name: &'static str, args: std::fmt::Arguments) -> Self { + let value = match args.as_str() { + Some(static_str) => static_str, + None => { + use bumpalo::core_alloc::fmt::Write; + let mut s = bumpalo::collections::String::new_in(self.ctx.bump()); + s.write_fmt(args).unwrap(); + s.into_bump_str() + } + }; + self.attributes.push(Attribute { name, value }); self } @@ -578,6 +587,12 @@ impl<'a> IntoDomTree<'a> for () { } } +impl<'a> IntoDomTree<'a> for Option<()> { + fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + VNode::Suspended + } +} + /// Construct a text VNode. /// /// This is `dioxus`'s virtual DOM equivalent of `document.createTextVNode`. @@ -599,6 +614,26 @@ pub fn text2<'a>(contents: bumpalo::collections::String<'a>) -> VNode<'a> { VNode::text(f) } +pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a> { + // This is a cute little optimization + // + // We use format_args! on everything. However, not all textnodes have dynamic content, and thus are completely static. + // we can just short-circuit to the &'static str version instead of having to allocate in the bump arena. + // + // In the most general case, this prevents the need for any string allocations for simple code IE: + // div {"abc"} + // + match args.as_str() { + Some(static_str) => VNode::text(static_str), + None => { + use bumpalo::core_alloc::fmt::Write; + let mut s = bumpalo::collections::String::new_in(bump); + s.write_fmt(args).unwrap(); + VNode::text(s.into_bump_str()) + } + } +} + /// Construct an attribute for an element. /// /// # Example diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e883d112..76fc9295 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -14,7 +14,7 @@ use std::{cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc}; /// It's a placeholder over vnodes, to make working with lifetimes easier pub struct DomTree { // this should *never* be publicly accessible to external - pub(crate) root: VNode<'static>, + pub root: VNode<'static>, } /// Tools for the base unit of the virtual dom - the VNode @@ -80,6 +80,9 @@ impl<'a> VNode<'a> { VNode::Suspended => { todo!() } + // Self::PhantomChild { id } => { + // todo!() + // } VNode::Component(c) => c.key, } } @@ -218,6 +221,8 @@ pub struct VComponent<'src> { pub comparator: Rc bool + 'src>, pub caller: Rc DomTree + 'src>, + pub children: &'src [VNode<'src>], + // a pointer into the bump arena (given by the 'src lifetime) raw_props: *const (), @@ -266,6 +271,7 @@ impl<'a> VComponent<'a> { user_fc: caller_ref, raw_props: props as *const P as *const _, _p: PhantomData, + children: &[], caller, comparator: Rc::new(props_comparator), stable_addr: Rc::new(RefCell::new(None)), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ac2edeb7..7722fc82 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -482,7 +482,7 @@ pub struct Scope { // IDs of children that this scope has created // This enables us to drop the children and their children when this scope is destroyed - pub children: RefCell>, + children: RefCell>, // A reference to the list of components. // This lets us traverse the component list whenever we need to access our parent or children. @@ -595,6 +595,14 @@ impl Scope { // This breaks any latent references, invalidating every pointer referencing into it. self.frames.next().bump.reset(); + // Remove all the outdated listeners + // + self.listeners + .try_borrow_mut() + .ok() + .ok_or(Error::FatalInternal("Borrowing listener failed"))? + .drain(..); + *self.hookidx.borrow_mut() = 0; let caller = self @@ -621,7 +629,7 @@ impl Scope { let EventTrigger { listener_id, event, .. } = trigger; - + // unsafe { // Convert the raw ptr into an actual object // This operation is assumed to be safe @@ -637,15 +645,6 @@ impl Scope { // Run the callback with the user event listener_fn(event); - - // drain all the event listeners - // if we don't, then they'll stick around and become invalid - // big big big big safety issue - self.listeners - .try_borrow_mut() - .ok() - .ok_or(Error::FatalInternal("Borrowing listener failed"))? - .drain(..); } Ok(()) } @@ -687,7 +686,7 @@ pub type Context<'src> = &'src Scope; impl Scope { /// Access the children elements passed into the component - pub fn children(&self) -> Vec { + pub fn children(&self) -> &[VNode] { todo!("Children API not yet implemented for component Context") } @@ -743,6 +742,17 @@ impl Scope { }, } } + + pub fn render2<'scope, F: for<'b> FnOnce(&'b NodeCtx<'scope>) -> VNode<'scope> + 'scope>( + &'scope self, + lazy_nodes: LazyNodes<'scope, F>, + ) -> VNode<'scope> { + let ctx = NodeCtx { + scope_ref: self, + listener_id: 0.into(), + }; + lazy_nodes.into_vnode(&ctx) + } } // ================================================ @@ -863,60 +873,66 @@ impl Scope { } /// There are hooks going on here! - pub fn use_context(&self) -> Rc { + pub fn use_context<'a, T: 'static>(&'a self) -> &'a Rc { self.try_use_context().unwrap() } - /// - pub fn try_use_context(&self) -> Result> { - let ty = TypeId::of::(); - - let mut scope = Some(self); - let cached_root = use_ref(self, || None as Option>); - - // Try to provide the cached version without having to re-climb the tree - if let Some(ptr) = cached_root.borrow().as_ref() { - if let Some(pt) = ptr.clone().upgrade() { - return Ok(pt); - } else { - /* - failed to upgrade the weak is strange - this means the root dropped the context (the scope was killed) - - The main idea here is to prevent memory leaks where parents should be cleaning up their own memory. - - However, this behavior allows receivers/providers to move around in the hierarchy. - This works because we climb the tree if upgrading the Rc failed. - */ - } + /// Uses a context, storing the cached value around + pub fn try_use_context(&self) -> Result<&Rc> { + struct UseContextHook { + par: Option>, + we: Option>, } - while let Some(inner) = scope { - log::debug!("Searching {:#?} for valid shared_context", inner.arena_idx); - let shared_contexts = inner.shared_contexts.borrow(); - if let Some(shared_ctx) = shared_contexts.get(&ty) { - let rc = shared_ctx - .downcast_ref() - .expect("Should not fail, already validated the type from the hashmap"); + self.use_hook( + move || UseContextHook { + par: None as Option>, + we: None as Option>, + }, + move |hook| { + let mut scope = Some(self); - *cached_root.borrow_mut() = Some(Rc::downgrade(&rc)); - return Ok(rc.clone()); - } else { - match inner.parent { - Some(parent_id) => { - let parent = inner - .arena_link - .try_get(parent_id) - .map_err(|_| Error::FatalInternal("Failed to find parent"))?; - - scope = Some(parent); + if let Some(we) = &hook.we { + if let Some(re) = we.upgrade() { + hook.par = Some(re); + return Ok(hook.par.as_ref().unwrap()); } - None => return Err(Error::MissingSharedContext), } - } - } - Err(Error::MissingSharedContext) + let ty = TypeId::of::(); + while let Some(inner) = scope { + log::debug!("Searching {:#?} for valid shared_context", inner.arena_idx); + let shared_contexts = inner.shared_contexts.borrow(); + + if let Some(shared_ctx) = shared_contexts.get(&ty) { + log::debug!("found matching ctx"); + let rc = shared_ctx + .clone() + .downcast::() + .expect("Should not fail, already validated the type from the hashmap"); + + hook.we = Some(Rc::downgrade(&rc)); + hook.par = Some(rc); + return Ok(hook.par.as_ref().unwrap()); + } else { + match inner.parent { + Some(parent_id) => { + let parent = inner + .arena_link + .try_get(parent_id) + .map_err(|_| Error::FatalInternal("Failed to find parent"))?; + + scope = Some(parent); + } + None => return Err(Error::MissingSharedContext), + } + } + } + + Err(Error::MissingSharedContext) + }, + |_| {}, + ) } } diff --git a/packages/liveview/index.html b/packages/liveview/index.html index 258542db..a389534a 100644 --- a/packages/liveview/index.html +++ b/packages/liveview/index.html @@ -136,6 +136,8 @@ interp.stack.push(document.createElement(tagName)); } + + // 11 NewEventListener(edit, interp) { // todo! @@ -186,7 +188,7 @@ } // 16 - CreateElementNS(edit, interp) { + CreateElementNs(edit, interp) { // const tagNameId = mem32[i++]; // const tagName = interp.getCachedString(tagNameId); // const nsId = mem32[i++]; @@ -280,7 +282,9 @@ function EditListReceived(rawEditList) { let editList = JSON.parse(rawEditList); + console.warn("hnelllo"); editList.forEach(function (edit, index) { + console.log(edit); op_table[edit.type](edit, interpreter); }); } diff --git a/packages/recoil/Cargo.toml b/packages/recoil/Cargo.toml index 7ef9e963..105019bf 100644 --- a/packages/recoil/Cargo.toml +++ b/packages/recoil/Cargo.toml @@ -9,10 +9,14 @@ edition = "2018" [dependencies] anyhow = "1.0.40" dioxus-core = { path = "../core" } -generational-arena = "0.2.8" thiserror = "1.0.24" +log = "0.4.14" +im-rc = "15.0.0" [dev-dependencies] -uuid = { version = "0.8.2", features = ["v4"] } +uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } dioxus-web = { path = "../web" } wasm-bindgen-futures = "*" + +wasm-logger = "0.2.0" +console_error_panic_hook = "0.1.6" diff --git a/packages/recoil/README.md b/packages/recoil/README.md index d388c076..792f8d5a 100644 --- a/packages/recoil/README.md +++ b/packages/recoil/README.md @@ -6,12 +6,14 @@ Recoil.rs is officially supported by the Dioxus team. By doing so, are are "plan Internally, Dioxus uses batching to speed up linear-style operations. Recoil.rs integrates with this batching optimization, making app-wide changes extremely fast. This way, Recoil.rs can be pushed significantly harder than Redux without the need to enable/disable debug flags to prevent performance slowdowns. -## Guide +# Guide + +## Atoms A simple atom of state is defined globally as a const: ```rust -const Light: Atom<&'static str> = |_| "Green"; +const Light: Atom<&str> = |_| "Green"; ``` This atom of state is initialized with a value of `"Green"`. The atom that is returned does not actually contain any values. Instead, the atom's key - which is automatically generated in this instance - is used in the context of a Recoil App. @@ -30,3 +32,106 @@ fn App(ctx: Context, props: &()) -> DomTree { }) } ``` + +Atoms are considered "Writable" objects since any consumer may also set the Atom's value with their own: + +```rust +fn App(ctx: Context, props: &()) -> DomTree { + let color = recoil::use_read(ctx, Light); + let set_color = recoil::use_write(ctx, Light); + rsx!{in ctx, + div { + h1{"Color: {color}"} + button {onclick: move |_| set_color("red"), "red"} + button {onclick: move |_| set_color("yellow"), "yellow"} + button {onclick: move |_| set_color("green"), "green"} + } + } +} +``` + +"Reading" a value with use_read subscribes that component to any changes to that value while "Writing" a value does not. It's a good idea to use `write-only` whenever it makes sense to prevent unnecessary evaluations. Both `read` and `write` may be combined together to provide a `use_state` style hook. + +```rust +fn App(ctx: Context, props: &()) -> DomTree { + let (color, set_color) = recoil::use_read_write(ctx, Light); + rsx!{in ctx, + div { + h1{"Color: {color}"} + button {onclick: move |_| set_color("red"), "red"} + } + } +} +``` + +## Selectors + +Selectors are a concept popular in the JS world as a way of narrowing down a selection of state outside the VDOM lifecycle. Selectors have two functions: 1) summarize/narrow down some complex state and 2) memoize calculations. + +Selectors are only `readable` - they cannot be set. This differs from RecoilJS where selectors _are_ `writable`. Selectors, as you might've guessed, "select" some data from atoms and other selectors. + +Selectors provide a `SelectorApi` which essentially exposes a read-only `RecoilApi`. They have the `get` method which allows any readable valued to be obtained for the purpose of generating a new value. A `Selector` may only return `'static` data, however `SelectorBorrowed` may return borrowed data. + +returning static data: + +```rust +const Light: Atom<&'static str> = |_| "Green"; +const NumChars: Selector = |api| api.get(&Light).len(); +``` + +Selectors will update their selection and notify subscribed components whenever their dependencies also update. The `get` calls in a selector will subscribe that selector to whatever `Readable` is being `get`-ted. Unlike hooks, you may use `get` in conditionals; an internal algorithm decides when a selector needs to be updated based on what it `get`-ted last time it was ran. + +Selectors may also returned borrowed data: + +```rust +const Light: Atom<&'static str> = |_| "Green"; +const ThreeChars: SelectorBorrowed = |api| api.get(&Light).chars().take(3).unwrap(); +``` + +Unfortunately, Rust tries to coerce `T` (the type in the Selector generics) to the `'static` lifetime because we defined it as a static. `SelectorBorrowed` defines a type for a function that returns `&T` and provides the right lifetime for `T`. If you don't like having to use a dedicated `Borrowed` type, regular functions work too: + +```rust +fn Light(api: &mut AtomBuilder) -> &'static str { + "Green" +} +fn ThreeChars(api: &mut SelectorApi) -> &'static str { + api.get(&Light).chars().take(3).unwrap() +} +``` + +Returning borrowed data is generally frowned upon, but may be useful when used wisely. + +- If a selected value equals its previous selection (via PartialEq), the old value must be kept around to avoid evaluating subscribed components. +- It's unlikely that a change in a dependency's data will not change the selector's output. + +In general, borrowed selectors introduce a slight memory overhead as they need to retain previous state to safely memoize downstream subscribers. The amount of state kept around scales with the amount of `gets` in a selector - though as the number of dependencies increase, the less likely the selector actually stays memoized. Recoil tries to optimize this behavior the best it can to balance component evaluations with memory overhead. + +## Families + +You might notice that collections will not be performant with just sets/gets. We don't want to clone our entire HashMap every time we want to insert or remove an entry! That's where `Families` come in. Families are memoized collections (HashMap and OrdMap) that wrap the immutable datastructures library `im-rc`. Families are defined by a function that takes the FamilyApi and returns a function that provides a default value given a key. In this example, we insert a value into the collection when initialized, and then return a function that takes a key and returns a default value. + +```rust +const CloudRatings: AtomFamily<&str, i32> = |api| { + api.insert("Oracle", -1); + |key| match key { + "AWS" => 1, + "Azure" => 2, + "GCP" => 3, + _ => 0 + } +} +``` + +Whenever you `select` on a `Family`, the ID of the entry is tracked. Other subscribers will only be updated if they too select the same family with the same key and that value is updated. Otherwise, these subscribers will never re-render on an "insert", "remove", or "update" of the collection. You could easily implement this yourself with Atoms, immutable datastructures, and selectors, but our implementation is more efficient and first-class. + +```rust +fn App(ctx: Context, props: &()) -> DomTree { + let (rating, set_rating) = recoil::use_read_write(ctx, CloudRatings.select("AWS")); + rsx!{in ctx, + div { + h1{ "AWS rating: {rating}" } + button { onclick: move |_| set_rating((rating + 1) % 5), "incr" } + } + } +} +``` diff --git a/packages/recoil/examples/family.rs b/packages/recoil/examples/family.rs index 396c9bf9..287c2c6f 100644 --- a/packages/recoil/examples/family.rs +++ b/packages/recoil/examples/family.rs @@ -1,27 +1,34 @@ -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc}; use dioxus_core::prelude::*; use recoil::*; use uuid::Uuid; -const TODOS: AtomFamily = |_| HashMap::new(); +const TODOS: AtomHashMap> = |map| {}; #[derive(PartialEq)] struct Todo { checked: bool, title: String, - contents: String, + content: String, } static App: FC<()> = |ctx, _| { - use_init_recoil_root(ctx, |_| {}); + use_init_recoil_root(ctx, move |cfg| {}); + let todos = use_read_family(ctx, &TODOS); - rsx! { in ctx, - div { - "Basic Todolist with AtomFamilies in Recoil.rs" - } - } + // rsx! { in ctx, + // div { + // h1 {"Basic Todolist with AtomFamilies in Recoil.rs"} + // ul { { todos.keys().map(|id| rsx! { Child { id: *id } }) } } + // } + // } + ctx.render(html! { + + Jade + + }) }; #[derive(Props, PartialEq)] @@ -33,11 +40,11 @@ static Child: FC = |ctx, props| { let (todo, set_todo) = use_read_write(ctx, &TODOS.select(&props.id)); rsx! { in ctx, - div { + li { h1 {"{todo.title}"} input { type: "checkbox", name: "scales", checked: "{todo.checked}" } - label { "{todo.contents}", for: "scales" } - p {"{todo.contents}"} + label { "{todo.content}", for: "scales" } + p {"{todo.content}"} } } }; diff --git a/packages/recoil/examples/familypoc.rs b/packages/recoil/examples/familypoc.rs new file mode 100644 index 00000000..2181492f --- /dev/null +++ b/packages/recoil/examples/familypoc.rs @@ -0,0 +1,47 @@ +use dioxus_core::prelude::*; +use im_rc::HashMap as ImMap; +use recoil::*; +use uuid::Uuid; + +const TODOS: Atom> = |_| ImMap::new(); + +#[derive(PartialEq)] +struct Todo { + checked: bool, + title: String, + contents: String, +} + +static App: FC<()> = |ctx, _| { + use_init_recoil_root(ctx, |_| {}); + let todos = use_read(ctx, &TODOS); + + rsx! { in ctx, + div { + "Basic Todolist with AtomFamilies in Recoil.rs" + } + } +}; + +#[derive(Props, PartialEq)] +struct ChildProps { + id: Uuid, +} + +static Child: FC = |ctx, props| { + let todo = use_read(ctx, &TODOS).get(&props.id).unwrap(); + // let (todo, set_todo) = use_read_write(ctx, &TODOS); + + rsx! { in ctx, + div { + h1 {"{todo.title}"} + input { type: "checkbox", name: "scales", checked: "{todo.checked}" } + label { "{todo.contents}", for: "scales" } + p {"{todo.contents}"} + } + } +}; + +fn main() { + wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App)) +} diff --git a/packages/recoil/examples/helloworld.rs b/packages/recoil/examples/hellorecoil.rs similarity index 60% rename from packages/recoil/examples/helloworld.rs rename to packages/recoil/examples/hellorecoil.rs index b8c73086..51fd3d39 100644 --- a/packages/recoil/examples/helloworld.rs +++ b/packages/recoil/examples/hellorecoil.rs @@ -11,12 +11,19 @@ static App: FC<()> = |ctx, _| { rsx! { in ctx, div { "Count: {count}" - button { onclick: move |_| set_count(count + 1), "Incr" } - button { onclick: move |_| set_count(count - 1), "Decr" } + br {} + button { onclick: move |_| set_count(count + 1), "___<" + button { onclick: move |_| set_count(count - 1), "Decr>" } } } }; fn main() { - wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App)) + // Setup logging + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + + log::debug!("asd"); + wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App)); } diff --git a/packages/recoil/examples/selectorlist.rs b/packages/recoil/examples/selectorlist.rs index ebe68b83..bcac7e48 100644 --- a/packages/recoil/examples/selectorlist.rs +++ b/packages/recoil/examples/selectorlist.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use dioxus_core::prelude::*; use recoil::*; -const A_ITEMS: AtomFamily = |_| HashMap::new(); -const B_ITEMS: AtomFamily = |_| HashMap::new(); +const A_ITEMS: AtomHashMap = |_| HashMap::new(); +const B_ITEMS: AtomHashMap = |_| HashMap::new(); const C_SELECTOR: SelectorFamily = |api, key| { let a = api.get(&A_ITEMS.select(&key)); diff --git a/packages/recoil/examples/test.html b/packages/recoil/examples/test.html new file mode 100644 index 00000000..5a664c9f --- /dev/null +++ b/packages/recoil/examples/test.html @@ -0,0 +1 @@ +println!("asd") diff --git a/packages/recoil/notes/Architecture.md b/packages/recoil/notes/Architecture.md index 5748a4fb..33a98f9f 100644 --- a/packages/recoil/notes/Architecture.md +++ b/packages/recoil/notes/Architecture.md @@ -95,8 +95,8 @@ const ContentCards: SelectorFamily = |api, key| api.on_get_as static ContentCard: FC<()> = |ctx, props| { let body = async match use_recoil_value()(props.id).await { - Ok(content) => rsx!{ p {"{content}"} } - Err(e) => rsx!{ p {"Failed to load"}} + Ok(content) => rsx!{in ctx, p {"{content}"} } + Err(e) => rsx!{in ctx, p {"Failed to load"}} }; rsx!{ diff --git a/packages/recoil/src/lib.rs b/packages/recoil/src/lib.rs index a4200f53..2c958a75 100644 --- a/packages/recoil/src/lib.rs +++ b/packages/recoil/src/lib.rs @@ -6,7 +6,6 @@ use std::{ rc::Rc, }; -pub use api::*; pub use atomfamily::*; pub use atoms::*; pub use ecs::*; @@ -18,13 +17,14 @@ pub use selector::*; pub use selectorfamily::*; pub use traits::*; pub use utils::*; +mod tracingimmap; mod traits { use dioxus_core::prelude::Context; use super::*; - pub trait FamilyKey: PartialEq + Hash + 'static {} - impl FamilyKey for T {} + pub trait MapKey: PartialEq + Hash + 'static {} + impl MapKey for T {} pub trait AtomValue: PartialEq + 'static {} impl AtomValue for T {} @@ -32,8 +32,7 @@ mod traits { // Atoms, selectors, and their family variants are readable pub trait Readable: Sized + Copy { fn use_read<'a>(self, ctx: Context<'a>) -> &'a T { - hooks::use_read(ctx, self); - todo!() + hooks::use_read(ctx, self) } // This returns a future of the value @@ -44,9 +43,7 @@ mod traits { todo!() } - fn initialize(self, api: &RecoilRoot) -> T { - todo!() - } + fn initialize(self, api: &RecoilRoot) -> T; // We use the Raw Ptr to the atom // TODO: Make sure atoms with the same definitions don't get merged together. I don't think they do, but double check @@ -78,7 +75,13 @@ mod atoms { // impl Readable for Atom {} impl Readable for &'static Atom { fn static_id(self) -> u32 { - todo!() + self as *const _ as u32 + } + + fn initialize(self, api: &RecoilRoot) -> T { + let mut builder = AtomBuilder {}; + let p = self(&mut builder); + p } } @@ -88,13 +91,13 @@ mod atoms { use super::*; use dioxus_core::prelude::Context; - const Example: Atom = |_| 10; + fn _test(ctx: Context) { + const EXAMPLE_ATOM: Atom = |_| 10; - fn test(ctx: Context) { // ensure that atoms are both read and write - let _ = use_read(ctx, &Example); - let _ = use_read_write(ctx, &Example); - let _ = use_write(ctx, &Example); + let _ = use_read(ctx, &EXAMPLE_ATOM); + let _ = use_read_write(ctx, &EXAMPLE_ATOM); + let _ = use_write(ctx, &EXAMPLE_ATOM); } } } @@ -104,38 +107,57 @@ mod atomfamily { pub trait FamilyCollection {} impl FamilyCollection for HashMap {} - pub type AtomFamily> = fn((&K, &V)) -> F; + use im_rc::HashMap as ImHashMap; - pub trait AtomFamilySelector { - fn select(&'static self, k: &K) -> AtomFamilySelection { + /// AtomHashMaps provide an efficient way of maintaing collections of atoms. + /// + /// Under the hood, AtomHashMaps uses [IM](https://www.rust-lang.org)'s immutable HashMap implementation to lazily + /// clone data as it is modified. + /// + /// + /// + /// + /// + /// + pub type AtomHashMap = fn(&mut ImHashMap); + + pub trait AtomFamilySelector { + fn select(&'static self, k: &K) -> AtomMapSelection { todo!() } } - impl AtomFamilySelector for AtomFamily { - fn select(&'static self, k: &K) -> AtomFamilySelection { + impl AtomFamilySelector for AtomHashMap { + fn select(&'static self, k: &K) -> AtomMapSelection { todo!() } } - pub struct AtomFamilySelection<'a, K: FamilyKey, V: AtomValue> { - root: &'static AtomFamily, + pub struct AtomMapSelection<'a, K: MapKey, V: AtomValue> { + root: &'static AtomHashMap, key: &'a K, } - impl<'a, K: FamilyKey, V: AtomValue> Readable for &AtomFamilySelection<'a, K, V> { + impl<'a, K: MapKey, V: AtomValue> Readable for &AtomMapSelection<'a, K, V> { fn static_id(self) -> u32 { todo!() } + + fn initialize(self, api: &RecoilRoot) -> V { + todo!() + // let mut builder = AtomBuilder {}; + // let p = self(&mut builder); + // p + } } - impl<'a, K: FamilyKey, T: AtomValue> Writable for &AtomFamilySelection<'a, K, T> {} + impl<'a, K: MapKey, T: AtomValue> Writable for &AtomMapSelection<'a, K, T> {} mod compiletests { use dioxus_core::prelude::Context; use super::*; - const Titles: AtomFamily = |_| HashMap::new(); + const Titles: AtomHashMap = |map| {}; fn test(ctx: Context) { let title = Titles.select(&10).use_read(ctx); @@ -157,6 +179,10 @@ mod selector { fn static_id(self) -> u32 { todo!() } + + fn initialize(self, api: &RecoilRoot) -> T { + todo!() + } } pub struct SelectorFamilyBuilder {} @@ -184,6 +210,10 @@ mod selectorfamily { fn static_id(self) -> u32 { todo!() } + + fn initialize(self, api: &RecoilRoot) -> V { + todo!() + } } /// Borrowed selector families are – surprisingly - discouraged. @@ -196,32 +226,12 @@ mod selectorfamily { // impl<'a, K, V: 'a> SelectionSelector for fn(&'a mut SelectorFamilyBuilder, K) -> V {} } -mod api { - use super::*; - - // pub struct RecoilApi {} - // impl RecoilApi { - // pub fn get(&self, t: &'static Atom) -> Rc { - // todo!() - // } - // pub fn modify( - // &self, - // t: &'static Atom, - // f: impl FnOnce(&mut T) -> O, - // ) -> O { - // todo!() - // } - // pub fn set(&self, t: &'static Atom, new: T) { - // self.modify(t, move |old| *old = new); - // } - // } -} - mod root { use std::{ any::{Any, TypeId}, collections::{HashSet, VecDeque}, iter::FromIterator, + sync::atomic::{AtomicU32, AtomicUsize}, }; use super::*; @@ -229,17 +239,7 @@ mod root { type AtomId = u32; type ConsumerId = u32; - pub struct RecoilContext { - pub(crate) inner: Rc>, - } - - impl RecoilContext { - pub fn new() -> Self { - Self { - inner: Rc::new(RefCell::new(RecoilRoot::new())), - } - } - } + pub type RecoilContext = RefCell; // Sometimes memoization means we don't need to re-render components that holds "correct values" // IE we consider re-render more expensive than keeping the old value around. @@ -248,7 +248,8 @@ mod root { // Instead, we choose to let the hook itself hold onto the Rc by not forcing a render when T is the same. // Whenever the component needs to be re-rendered for other reasons, the "get" method will automatically update the Rc to the most recent one. pub struct RecoilRoot { - nodes: HashMap, + nodes: RefCell>, + consumer_map: HashMap, } struct Slot { @@ -259,23 +260,40 @@ mod root { dependents: HashSet, } + static NEXT_ID: AtomicU32 = AtomicU32::new(0); + fn next_consumer_id() -> u32 { + NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + impl RecoilRoot { pub(crate) fn new() -> Self { Self { nodes: Default::default(), + consumer_map: Default::default(), } } pub fn subscribe( - &self, + &mut self, readable: impl Readable, receiver_fn: Rc, ) -> ConsumerId { - todo!() + let consumer_id = next_consumer_id(); + let atom_id = readable.static_id(); + log::debug!("Subscribing consumer to atom {} {}", consumer_id, atom_id); + + let mut nodes = self.nodes.borrow_mut(); + let slot = nodes.get_mut(&atom_id).unwrap(); + slot.consumers.insert(consumer_id, receiver_fn); + self.consumer_map.insert(consumer_id, atom_id); + consumer_id } - pub fn unsubscribe(&self, id: ConsumerId) { - todo!() + pub fn unsubscribe(&mut self, consumer_id: ConsumerId) { + let atom_id = self.consumer_map.get(&consumer_id).unwrap(); + let mut nodes = self.nodes.borrow_mut(); + let slot = nodes.get_mut(&atom_id).unwrap(); + slot.consumers.remove(&consumer_id); } /// Directly get the *slot* @@ -283,12 +301,28 @@ mod root { /// /// pub fn try_get_raw(&self, readable: impl Readable) -> Result> { - todo!() - } + let atom_id = readable.static_id(); + let mut nodes = self.nodes.borrow_mut(); + if !nodes.contains_key(&atom_id) { + let value = Slot { + type_id: TypeId::of::(), + source: atom_id, + value: Rc::new(readable.initialize(self)), + consumers: Default::default(), + dependents: Default::default(), + }; + nodes.insert(atom_id, value); + } + let out = nodes + .get(&atom_id) + .unwrap() + .value + .clone() + .downcast::() + .unwrap(); - // pub fn try_get(&self, readable: impl Readable) -> Result<&T> { - // self.try_get_raw(readable).map(|f| f.as_ref()) - // } + Ok(out) + } pub fn try_set( &mut self, @@ -297,7 +331,15 @@ mod root { ) -> crate::error::Result<()> { let atom_id = writable.static_id(); - let consumers = match self.nodes.get_mut(&atom_id) { + self.set_by_id(atom_id, new_val); + + Ok(()) + } + + // A slightly dangerous method to manually overwrite any slot given an AtomId + pub(crate) fn set_by_id(&mut self, atom_id: AtomId, new_val: T) { + let mut nodes = self.nodes.borrow_mut(); + let consumers = match nodes.get_mut(&atom_id) { Some(slot) => { slot.value = Rc::new(new_val); &slot.consumers @@ -306,44 +348,30 @@ mod root { let value = Slot { type_id: TypeId::of::(), source: atom_id, - value: Rc::new(writable.initialize(self)), + value: Rc::new(new_val), + // value: Rc::new(writable.initialize(self)), consumers: Default::default(), dependents: Default::default(), }; - self.nodes.insert(atom_id, value); - &self.nodes.get(&atom_id).unwrap().consumers + nodes.insert(atom_id, value); + &nodes.get(&atom_id).unwrap().consumers } }; - for (_, consumer_fn) in consumers { + for (id, consumer_fn) in consumers { + log::debug!("triggering selector {}", id); consumer_fn(); } - - // if it's a an atom or selector, update all the dependents - - Ok(()) } - - pub fn get(&self, readable: impl Readable) -> Rc { - todo!() - // self.try_get(readable).unwrap() - } - - pub fn set(&mut self, writable: impl Writable, new_val: T) { - self.try_set(writable, new_val).unwrap(); - } - - /// A slightly dangerous method to manually overwrite any slot given an AtomId - pub(crate) fn set_by_id(&self, id: AtomId, new_val: T) {} } } mod hooks { use super::*; - use dioxus_core::prelude::Context; + use dioxus_core::{hooks::use_ref, prelude::Context}; pub fn use_init_recoil_root(ctx: Context, cfg: impl Fn(())) { - ctx.use_create_context(move || RecoilRoot::new()) + ctx.use_create_context(move || RefCell::new(RecoilRoot::new())) } /// Gain access to the recoil API directly - set, get, modify, everything @@ -353,25 +381,28 @@ mod hooks { /// /// You can use this method to create controllers that perform much more complex actions than set/get /// However, be aware that "getting" values through this hook will not subscribe the component to any updates. - pub fn use_recoil_api<'a, F: 'a>( - ctx: Context<'a>, - f: impl Fn(Rc) -> F + 'static, - ) -> &F { - let g = ctx.use_context::(); - let api = g.inner.clone(); - todo!() + pub fn use_recoil_api<'a>(ctx: Context<'a>) -> &Rc { + ctx.use_context::() } pub fn use_write<'a, T: AtomValue>( ctx: Context<'a>, + // todo: this shouldn't need to be static writable: impl Writable, ) -> &'a Rc { - let api = use_recoil_api(ctx, |f| f); + let api = use_recoil_api(ctx); ctx.use_hook( move || { let api = api.clone(); let raw_id = writable.static_id(); - Rc::new(move |new_val| api.set_by_id(raw_id, new_val)) as Rc + Rc::new(move |new_val| { + // + log::debug!("setting new value "); + let mut api = api.as_ref().borrow_mut(); + + // api.try_set(writable, new_val).expect("failed to set"); + api.set_by_id(raw_id, new_val); + }) as Rc }, move |hook| &*hook, |hook| {}, @@ -387,9 +418,11 @@ mod hooks { consumer_id: u32, } - let api = use_recoil_api(ctx, |api| api); + let api = use_recoil_api(ctx); ctx.use_hook( move || { + let mut api = api.as_ref().borrow_mut(); + let update = ctx.schedule_update(); let val = api.try_get_raw(readable).unwrap(); let id = api.subscribe(readable, Rc::new(update)); @@ -399,11 +432,14 @@ mod hooks { } }, move |hook| { + let api = api.as_ref().borrow(); + let val = api.try_get_raw(readable).unwrap(); hook.value = val; &hook.value }, - |hook| { + move |hook| { + let mut api = api.as_ref().borrow_mut(); api.unsubscribe(hook.consumer_id); }, ) @@ -414,12 +450,16 @@ mod hooks { use_read_raw(ctx, readable).as_ref() } - /// Use an atom in both read and write modes - only available for atoms and family selections (not selectors) + /// # Use an atom in both read and write + /// + /// This method is only available for atoms and family selections (not selectors). + /// /// This is equivalent to calling both `use_read` and `use_write`, but saves you the hassle and repitition /// + /// ## Example + /// /// ``` /// const Title: Atom<&str> = |_| "hello"; - /// //... /// let (title, set_title) = use_read_write(ctx, &Title); /// /// // equivalent to: @@ -432,13 +472,40 @@ mod hooks { (use_read(ctx, writable), use_write(ctx, writable)) } + /// # Modify an atom without using `use_read`. + /// + /// Occasionally, a component might want to write to an atom without subscribing to its changes. `use_write` does not + /// provide this functionality, so `use_modify` exists to gain access to the current atom value while setting it. + /// + /// ## Notes + /// + /// Do note that this hook can only be used with Atoms where T: Clone since we actually clone the current atom to make + /// it mutable. + /// + /// Also note that you need to stack-borrow the closure since the modify closure expects an &dyn Fn. If we made it + /// a static type, it wouldn't be possible to use the `modify` closure more than once (two closures always are different) + /// + /// ## Example + /// + /// ```ignore + /// let modify_atom = use_modify(ctx, Atom); + /// + /// modify_atom(&|a| *a += 1) + /// ``` + pub fn use_modify<'a, T: AtomValue + 'static + Clone>( + ctx: Context<'a>, + writable: impl Writable, + ) -> impl Fn(&dyn Fn()) { + |_| {} + } + /// Use a family collection directly /// !! Any changes to the family will cause this subscriber to update /// Try not to put this at the very top-level of your app. - pub fn use_read_family<'a, K, V, C: FamilyCollection>( + pub fn use_read_family<'a, K, V>( ctx: Context<'a>, - t: &AtomFamily, - ) -> &'a C { + t: &AtomHashMap, + ) -> &'a im_rc::HashMap { todo!() } } diff --git a/packages/recoil/src/tracingimmap.rs b/packages/recoil/src/tracingimmap.rs new file mode 100644 index 00000000..05d3db9b --- /dev/null +++ b/packages/recoil/src/tracingimmap.rs @@ -0,0 +1,29 @@ +//! Tracing immap +//! Traces modifications since last generation +//! To reconstruct the history, you will need *all* the generations between the start and end points + +use im_rc::HashMap as ImMap; + +pub struct TracedHashMap { + inner: ImMap, + generation: u32, + mods_since_last_gen: Vec, +} + +impl TracedHashMap { + fn next_generation(&self) -> Self { + Self { + generation: self.generation + 1, + inner: self.inner.clone(), + mods_since_last_gen: vec![], + } + } +} + +#[test] +fn compare_dos() { + let map1 = im_rc::hashmap! {3 => 2, 2 => 3}; + let map2 = im_rc::hashmap! {2 => 3, 3 => 2}; + + assert_eq!(map1, map2); +} diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index d7cb17fb..ba6818e2 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -2,13 +2,20 @@ use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + + log::info!("hello world"); wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } static Example: FC<()> = |ctx, _props| { - ctx.render(html! { -
- "Hello world!" -
+ ctx.render(rsx! { + div { + span { + class: "px-2 py-1 flex w-36 mt-4 items-center text-xs rounded-md font-semibold text-yellow-500 bg-yellow-100" + "DUE DATE : 18 JUN" + } + } }) }; diff --git a/packages/web/examples/htmlexample.html b/packages/web/examples/htmlexample.html new file mode 100644 index 00000000..e081b23e --- /dev/null +++ b/packages/web/examples/htmlexample.html @@ -0,0 +1,59 @@ +
+

+ "Messages" +

+
    +
  • + + profil + +
    + + {title} + + + "Hey John ! Do you read the NextJS doc ?" + +
    +
  • +
  • + + profil + +
    + + "Marie Lou" + + + "No I think the dog is better..." + +
    +
  • +
  • + + profil + +
    + + "Ivan Buck" + + + "Seriously ? haha Bob is not a children !" + +
    +
  • +
  • + + profil + +
    + + "Marina Farga" + + + "Do you need that deisgn ?" + +
    +
  • +
+
diff --git a/packages/web/examples/list.rs b/packages/web/examples/list.rs index 422ef24d..fe480107 100644 --- a/packages/web/examples/list.rs +++ b/packages/web/examples/list.rs @@ -38,7 +38,6 @@ static App: FC<()> = |ctx, _| { ctx.render(rsx!( div { id: "app" - style { "{APP_STYLE}" } div { header { diff --git a/packages/web/examples/todomvc_simple.rs b/packages/web/examples/todomvc_simple.rs index cfb9eb2c..74b67df3 100644 --- a/packages/web/examples/todomvc_simple.rs +++ b/packages/web/examples/todomvc_simple.rs @@ -30,7 +30,6 @@ pub fn App(ctx: Context, props: &()) -> DomTree { ctx.render(rsx! { div { id: "app" - style { "{APP_STYLE}" } // list TodoList {} @@ -101,7 +100,7 @@ pub struct TodoEntryProps { item: Rc, } -// pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { +pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { // #[inline_props] pub fn TodoEntry( ctx: Context, diff --git a/packages/web/examples/todomvcsingle.rs b/packages/web/examples/todomvcsingle.rs index 39d24e56..03585912 100644 --- a/packages/web/examples/todomvcsingle.rs +++ b/packages/web/examples/todomvcsingle.rs @@ -28,7 +28,7 @@ pub struct TodoItem { } // Declare our global app state -const TODO_LIST: Atom> = |_| Default::default(); +const TODO_LIST: AtomHashMap = |_| {}; const FILTER: Atom = |_| FilterState::All; const TODOS_LEFT: Selector = |api| api.get(&TODO_LIST).len(); @@ -74,8 +74,8 @@ impl TodoManager { pub fn TodoList(ctx: Context, _props: &()) -> DomTree { let draft = use_state_new(&ctx, || "".to_string()); - let todos = use_recoil_value(&ctx, &TODO_LIST); - let filter = use_recoil_value(&ctx, &FILTER); + let todos = use_read(&ctx, &TODO_LIST); + let filter = use_read(&ctx, &FILTER); let todolist = todos .values() @@ -91,8 +91,8 @@ pub fn TodoList(ctx: Context, _props: &()) -> DomTree { }) }); - rsx! { in ctuse_read - div {use_read + rsx! { in ctx, + div { header { class: "header" h1 {"todos"} @@ -118,7 +118,7 @@ pub struct TodoEntryProps { pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { let (is_editing, set_is_editing) = use_state(&ctx, || false); - let todo = use_recoil_value(&ctx, &TODO_LIST).get(&props.id).unwrap(); + let todo = use_read(&ctx, &TODO_LIST).get(&props.id).unwrap(); ctx.render(rsx! ( li { @@ -135,11 +135,11 @@ pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { ))} } )) -}use_read +} pub fn FilterToggles(ctx: Context, _props: &()) -> DomTree { - let reducer = use_recoil_callback(ctx, |api| TodoManager(api)); - let items_left = use_recoil_value(ctx, &TODOS_LEFT); + let reducer = TodoManager(use_recoil_api(ctx)); + let items_left = use_read(ctx, &TODOS_LEFT); let toggles = [ ("All", "", FilterState::All), @@ -198,9 +198,9 @@ pub fn Footer(ctx: Context, _props: &()) -> DomTree { const APP_STYLE: &'static str = include_str!("./todomvc/style.css"); fn App(ctx: Context, _props: &()) -> DomTree { - use_init_recoil_root(ctx); + use_init_recoil_root(ctx, |_| {}); rsx! { in ctx, - div { id: "app", style { "{APP_STYLE}" } + div { id: "app" TodoList {} Footer {} } diff --git a/packages/web/src/interpreter.rs b/packages/web/src/interpreter.rs index bb7d28e5..ab0a37c2 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/interpreter.rs @@ -276,8 +276,15 @@ impl PatchMachine { Edit::SetAttribute { name, value } => { let node = self.stack.top(); - if let Some(node) = node.dyn_ref::() { + log::debug!("setting attribute for element"); + + if let Some(node) = node.dyn_ref::() { + // node.set_attribute(name, value).unwrap(); + log::info!("setting attr {} {}", name, value); node.set_attribute(name, value).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + log::debug!("el is html input element"); // Some attributes are "volatile" and don't work through `setAttribute`. if name == "value" { @@ -289,18 +296,22 @@ impl PatchMachine { } if let Some(node) = node.dyn_ref::() { + log::debug!("el is html option element"); if name == "selected" { node.set_selected(true); } } + log::debug!("el is none"); + // if let Some(node) = node.dyn_ref::() {} } // 4 Edit::RemoveAttribute { name } => { let node = self.stack.top(); - if let Some(node) = node.dyn_ref::() { + if let Some(node) = node.dyn_ref::() { node.remove_attribute(name).unwrap(); - + } + if let Some(node) = node.dyn_ref::() { // Some attributes are "volatile" and don't work through `removeAttribute`. if name == "value" { node.set_value(""); @@ -433,6 +444,7 @@ impl PatchMachine { .unwrap() .dyn_into::() .unwrap(); + log::debug!("Made NS element {} {}", ns, tag_name); self.stack.push(el); } diff --git a/packages/webview/Cargo.toml b/packages/webview/Cargo.toml index be19ad8e..1ef708be 100644 --- a/packages/webview/Cargo.toml +++ b/packages/webview/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT/Apache-2.0" [dependencies] # web-view = { git = "https://github.com/Boscop/web-view" } web-view = "0.7.3" -dioxus-core = { path = "../core", version = "0.1.2", features = ["serde"] } +dioxus-core = { path = "../core", version = "0.1.2", features = ["serialize"] } anyhow = "1.0.38" argh = "0.1.4" serde = "1.0.120" diff --git a/packages/webview/examples/demo.rs b/packages/webview/examples/demo.rs index f29a3095..1f4d3233 100644 --- a/packages/webview/examples/demo.rs +++ b/packages/webview/examples/demo.rs @@ -18,29 +18,48 @@ fn main() { } static Example: FC<()> = |ctx, _props| { - ctx.render(rsx! { - div { - class: "flex items-center justify-center flex-col" - div { - class: "flex items-center justify-center" - div { - class: "flex flex-col bg-white rounded p-4 w-full max-w-xs" - div { class: "font-bold text-xl", "Example desktop app" } - div { class: "text-sm text-gray-500", "This is running natively" } - div { - class: "flex flex-row items-center justify-center mt-6" - div { class: "font-medium text-6xl", "100%" } - } - div { - class: "flex flex-row justify-between mt-6" - a { - href: "https://www.dioxuslabs.com" - class: "underline" - "Made with dioxus" - } - } - } - } - } + ctx.render(html! { +
+ + + + + + +
}) }; +// static Example: FC<()> = |ctx, _props| { +// ctx.render(rsx! { +// div { +// class: "flex items-center justify-center flex-col" +// div { +// class: "flex items-center justify-center" +// div { +// class: "flex flex-col bg-white rounded p-4 w-full max-w-xs" +// div { class: "font-bold text-xl", "Example desktop app" } +// div { class: "text-sm text-gray-500", "This is running natively" } +// div { +// class: "flex flex-row items-center justify-center mt-6" +// div { class: "font-medium text-6xl", "100%" } +// } +// div { +// class: "flex flex-row justify-between mt-6" +// a { +// href: "https://www.dioxuslabs.com" +// class: "underline" +// "Made with dioxus" +// } +// } +// } +// } +// } +// }) +// };