From 9810feebf57f93114e3d7faf6de053ac192593a9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 May 2021 12:03:08 -0400 Subject: [PATCH] Feat: major overhaul to diffing --- Cargo.toml | 19 +- notes/CHANGELOG.md | 13 +- packages/cli/Cargo.toml | 2 +- packages/core-macro/src/rsxt.rs | 396 +++++----- packages/core-macro/src/util.rs | 5 +- packages/core/Cargo.toml | 2 +- packages/core/examples/borrowed.rs | 6 +- packages/core/examples/dummy.rs | 102 --- packages/core/examples/fmter.rs | 63 -- packages/core/examples/macro_testing.rs | 54 ++ packages/core/src/context.rs | 12 +- packages/core/src/diff.rs | 33 +- packages/core/src/hooks.rs | 8 +- packages/core/src/lib.rs | 48 +- packages/core/src/patch.rs | 10 +- packages/core/src/scope.rs | 382 ---------- packages/core/src/virtual_dom.rs | 695 ++++++++++++++---- packages/hooks/src/lib.rs | 127 +++- packages/ios/Cargo.toml | 12 + packages/ios/README.md | 3 + packages/ios/src/lib.rs | 70 ++ packages/web/Cargo.toml | 4 +- packages/web/examples/deep.rs | 4 +- packages/web/examples/demo2.rs | 1 + packages/web/examples/demoday.rs | 86 +++ packages/web/examples/landingpage.rs | 43 ++ packages/web/examples/landingpage2.rs | 43 ++ packages/web/examples/list.rs | 256 ++++--- .../web/examples/todomvc/filtertoggles.rs | 16 +- packages/web/examples/todomvc/todoitem.rs | 2 +- packages/web/examples/todomvc_simple.rs | 7 - packages/web/examples/todomvcsingle.rs | 1 + packages/web/examples/weather.rs | 3 +- packages/web/src/interpreter.rs | 10 +- packages/web/src/lib.rs | 2 +- packages/webview/Cargo.toml | 2 +- packages/webview/examples/demo.rs | 57 +- 37 files changed, 1470 insertions(+), 1129 deletions(-) delete mode 100644 packages/core/examples/dummy.rs delete mode 100644 packages/core/examples/fmter.rs create mode 100644 packages/core/examples/macro_testing.rs delete mode 100644 packages/core/src/scope.rs create mode 100644 packages/ios/Cargo.toml create mode 100644 packages/ios/README.md create mode 100644 packages/ios/src/lib.rs create mode 100644 packages/web/examples/demo2.rs create mode 100644 packages/web/examples/demoday.rs create mode 100644 packages/web/examples/landingpage.rs create mode 100644 packages/web/examples/landingpage2.rs diff --git a/Cargo.toml b/Cargo.toml index ae9d2c46..3e92bd80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,28 @@ [workspace] +# members = ["packages/core-macro"] members = [ - "packages/dioxus", "packages/core-macro", "packages/core", "packages/web", - "packages/docsite", - "packages/ssr", + "packages/webview", + "packages/dioxus", ] + +# "packages/docsite", +# "packages/ssr", +# "packages/cli", +# "packages/webview", +# "packages/hooks", +# "packages/ios", + +[profile.dev] +split-debuginfo = "unpacked" + # "packages/liveview", # "packages/3d", # Built-in # "packages/webview/client", -# "packages/webview", # "packages/router", # "packages/webview", # "packages/livehost", @@ -22,7 +32,6 @@ members = [ # "packages/macro", # TODO @Jon, share the validation code # "packages/web", -# "packages/hooks", # "packages/cli", # "examples", # "packages/html-macro", diff --git a/notes/CHANGELOG.md b/notes/CHANGELOG.md index f4ce34ea..b2dec697 100644 --- a/notes/CHANGELOG.md +++ b/notes/CHANGELOG.md @@ -24,7 +24,7 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su > 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 -- [ ] (Macro) Allow components to specify their props as function args (not going to do) +- [ ] (Macro) Allow components to specify their props as function args ## Project: Hooks + Context + Subscriptions (TBD) > Implement the foundations for state management @@ -80,20 +80,21 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su - [x] Allow paths for components - [x] todo mvc - [ ] Make events lazy (use traits + Box) -- [ ] attrs on elements should implement format_args +- [ ] Attributes on elements should implement format_args - [ ] Beef up the dioxus CLI tool - [ ] Tweak macro parsing for better errors -- [ ] make ssr follow HTML spec (ish) +- [ ] make SSR follow HTML spec - [ ] dirty tagging, compression - [ ] fix keys on elements -- [ ] miri tests +- [ ] MIRI tests - [ ] code health - [ ] all synthetic events filled out -- [ ] doublecheck event targets and stuff +- [ ] double check event targets and stuff - [ ] Documentation overhaul - [ ] Website += [ ] controlled components -lower priorty features +lower priority features - [ ] fragments - [ ] node refs (postpone for future release?) - [ ] styling built-in (future release?) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 2b60832c..d75edf3f 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -12,7 +12,7 @@ description = "CLI tool for developing, testing, and publishing Dioxus apps" thiserror = "1.0.23" log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } -wasm-bindgen-cli-support = "0.2.71" +wasm-bindgen-cli-support = "0.2.73" anyhow = "1.0.38" argh = "0.1.4" serde = "1.0.120" diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index 41c17491..8dbdd36c 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -1,5 +1,7 @@ use syn::parse::{discouraged::Speculative, ParseBuffer}; +use crate::util::is_valid_html_tag; + use { proc_macro::TokenStream, proc_macro2::{Span, TokenStream as TokenStream2}, @@ -15,77 +17,105 @@ use { // Parse any stream coming from the rsx! macro // ============================================== pub struct RsxRender { - custom_context: Option, - root: RootOption, -} - -enum RootOption { - // for lists of components - Fragment(), - Element(Element), - Component(Component), -} - -impl ToTokens for RootOption { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - RootOption::Fragment() => todo!(), - RootOption::Element(el) => el.to_tokens(tokens), - RootOption::Component(comp) => comp.to_tokens(tokens), - } - } + // custom_context: Option, + root: AmbiguousElement, } impl Parse for RsxRender { fn parse(input: ParseStream) -> Result { - let fork = input.fork(); + let root = { input.parse::() }?; + if !input.is_empty() { + return Err(Error::new( + input.span(), + "Currently only one element is allowed per component", + )); + } - let custom_context = fork - .parse::() - .and_then(|ident| { - fork.parse::().map(|_| { - input.advance_to(&fork); - ident - }) - }) - .ok(); - - let forked = input.fork(); - let name = forked.parse::()?; - - let root = match crate::util::is_valid_tag(&name.to_string()) { - true => input.parse::().map(|el| RootOption::Element(el)), - false => input.parse::().map(|c| RootOption::Component(c)), - }?; - - Ok(Self { - root, - custom_context, - }) + Ok(Self { root }) } } impl ToTokens for RsxRender { fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let new_toks = (&self.root).to_token_stream(); - // create a lazy tree that accepts a bump allocator - let final_tokens = match &self.custom_context { - Some(ident) => quote! { - #ident.render(dioxus::prelude::LazyNodes::new(move |ctx|{ - let bump = ctx.bump; - #new_toks - })) - }, - None => quote! { - dioxus::prelude::LazyNodes::new(move |ctx|{ - let bump = ctx.bump; - #new_toks - }) - }, - }; + // Currently disabled + // + // let final_tokens = match &self.custom_context { + // Some(ident) => quote! { + // #ident.render(dioxus::prelude::LazyNodes::new(move |ctx|{ + // let bump = ctx.bump; + // #new_toks + // })) + // }, - final_tokens.to_tokens(out_tokens); + let inner = &self.root; + let output = quote! { + dioxus::prelude::LazyNodes::new(move |ctx|{ + let bump = ctx.bump; + #inner + }) + }; + output.to_tokens(out_tokens) + } +} + +enum AmbiguousElement { + Element(Element), + Component(Component), +} + +impl Parse for AmbiguousElement { + fn parse(input: ParseStream) -> Result { + // Try to parse as an absolute path and immediately defer to the componetn + if input.peek(Token![::]) { + return input + .parse::() + .map(|c| AmbiguousElement::Component(c)); + } + + // If not an absolute path, then parse the ident and check if it's a valid tag + + if let Ok(pat) = input.fork().parse::() { + if pat.segments.len() > 1 { + return input + .parse::() + .map(|c| AmbiguousElement::Component(c)); + } + } + + if let Ok(name) = input.fork().parse::() { + let name_str = name.to_string(); + + match is_valid_html_tag(&name_str) { + true => input + .parse::() + .map(|c| AmbiguousElement::Element(c)), + false => { + let first_char = name_str.chars().next().unwrap(); + if first_char.is_ascii_uppercase() { + input + .parse::() + .map(|c| AmbiguousElement::Component(c)) + } else { + Err(Error::new( + name.span(), + "Components must be uppercased, perhaps you mispelled a html tag", + )) + } + } + } + } else { + Err(Error::new(input.span(), "Not a valid Html tag")) + } + } +} + +impl ToTokens for AmbiguousElement { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + AmbiguousElement::Element(el) => el.to_tokens(tokens), + AmbiguousElement::Component(comp) => comp.to_tokens(tokens), + } } } @@ -93,9 +123,8 @@ impl ToTokens for RsxRender { // Parse any div {} as a VElement // ============================================== enum Node { - Element(Element), + Element(AmbiguousElement), Text(TextNode), - Component(Component), RawExpr(Expr), } @@ -104,7 +133,6 @@ impl ToTokens for &Node { match &self { Node::Element(el) => el.to_tokens(tokens), Node::Text(txt) => txt.to_tokens(tokens), - Node::Component(c) => c.to_tokens(tokens), Node::RawExpr(exp) => exp.to_tokens(tokens), } } @@ -115,31 +143,17 @@ impl Parse for Node { // Supposedly this approach is discouraged due to inability to return proper errors // TODO: Rework this to provide more informative errors - let fork = stream.fork(); - if let Ok(text) = fork.parse::() { - stream.advance_to(&fork); - return Ok(Self::Text(text)); + if stream.peek(token::Brace) { + let content; + syn::braced!(content in stream); + return Ok(Node::RawExpr(content.parse::()?)); } - let fork = stream.fork(); - if let Ok(element) = fork.parse::() { - stream.advance_to(&fork); - return Ok(Self::Element(element)); + if stream.peek(LitStr) { + return Ok(Node::Text(stream.parse::()?)); } - let fork = stream.fork(); - if let Ok(comp) = fork.parse::() { - stream.advance_to(&fork); - return Ok(Self::Component(comp)); - } - - let fork = stream.fork(); - if let Ok(tok) = try_parse_bracketed(&fork) { - stream.advance_to(&fork); - return Ok(Node::RawExpr(tok)); - } - - return Err(Error::new(stream.span(), "Failed to parse as a valid node")); + Ok(Node::Element(stream.parse::()?)) } } @@ -156,6 +170,7 @@ impl Parse for Component { // todo: look into somehow getting the crate/super/etc let name = syn::Path::parse_mod_style(s)?; + // parse the guts let content: ParseBuffer; syn::braced!(content in s); @@ -169,9 +184,7 @@ impl Parse for Component { break 'parsing; } - if let Ok(field) = content.parse::() { - body.push(field); - } + body.push(content.parse::()?); // consume comma if it exists // we don't actually care if there *are* commas between attrs @@ -228,6 +241,7 @@ impl ToTokens for &Component { }); } } + // the struct's fields info pub struct ComponentField { name: Ident, @@ -258,10 +272,56 @@ impl ToTokens for &ComponentField { // ======================================= struct Element { name: Ident, - attrs: Vec, + attrs: Vec, children: Vec, } +impl Parse for Element { + fn parse(stream: ParseStream) -> Result { + // + let name = Ident::parse(stream)?; + + if !crate::util::is_valid_html_tag(&name.to_string()) { + return Err(Error::new(name.span(), "Not a valid Html tag")); + } + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in stream); + + let mut attrs: Vec = vec![]; + let mut children: Vec = vec![]; + 'parsing: loop { + // [1] Break if empty + if content.is_empty() { + break 'parsing; + } + + let forked = content.fork(); + if forked.call(Ident::parse_any).is_ok() + && forked.parse::().is_ok() + && forked.parse::().is_ok() + { + attrs.push(content.parse::()?); + } else { + children.push(content.parse::()?); + } + + // consume comma if it exists + // we don't actually care if there *are* commas after elements/text + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + name, + attrs, + children, + }) + } +} + impl ToTokens for &Element { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name.to_string(); @@ -288,86 +348,22 @@ impl ToTokens for &Element { } } -impl Parse for Element { - fn parse(s: ParseStream) -> Result { - let name = Ident::parse_any(s)?; - - if !crate::util::is_valid_tag(&name.to_string()) { - return Err(Error::new(name.span(), "Not a valid Html tag")); - } - - // parse the guts - let content: ParseBuffer; - syn::braced!(content in s); - - let mut attrs: Vec = vec![]; - let mut children: Vec = vec![]; - parse_element_content(content, &mut attrs, &mut children)?; - - Ok(Self { - name, - attrs, - children, - }) - } -} - -// used by both vcomponet and velement to parse the contents of the elements into attras and children -fn parse_element_content( - stream: ParseBuffer, - attrs: &mut Vec, - children: &mut Vec, -) -> Result<()> { - 'parsing: loop { - // consume comma if it exists - // we don't actually care if there *are* commas after elements/text - if stream.peek(Token![,]) { - let _ = stream.parse::(); - } - - // [1] Break if empty - if stream.is_empty() { - break 'parsing; - } - - // [2] Try to consume an attr - let fork = stream.fork(); - if let Ok(attr) = fork.parse::() { - // make sure to advance or your computer will become a space heater :) - stream.advance_to(&fork); - attrs.push(attr); - continue 'parsing; - } - - // [3] Try to consume a child node - let fork = stream.fork(); - if let Ok(node) = fork.parse::() { - // make sure to advance or your computer will become a space heater :) - stream.advance_to(&fork); - children.push(node); - continue 'parsing; - } - - // todo: pass a descriptive error onto the offending tokens - panic!("Entry is not an attr or element\n {}", stream) - } - Ok(()) -} -fn try_parse_bracketed(stream: &ParseBuffer) -> Result { - let content; - syn::braced!(content in stream); - content.parse() -} - /// ======================================= /// Parse a VElement's Attributes /// ======================================= -struct Attr { +struct ElementAttr { name: Ident, ty: AttrType, } -impl Parse for Attr { +enum AttrType { + Value(LitStr), + FieldTokens(Expr), + EventTokens(Expr), + Event(ExprClosure), +} + +impl Parse for ElementAttr { fn parse(s: ParseStream) -> Result { let mut name = Ident::parse_any(s)?; let name_str = name.to_string(); @@ -389,32 +385,39 @@ impl Parse for Attr { content.advance_to(&fork); AttrType::Event(event) } else { - AttrType::Tok(content.parse()?) + AttrType::EventTokens(content.parse()?) } } else { AttrType::Event(s.parse()?) } } else { - let lit_str = if name_str == "style" && s.peek(token::Brace) { - // special-case to deal with literal styles. - let outer; - syn::braced!(outer in s); - // double brace for inline style. - // todo!("Style support not ready yet"); - - // if outer.peek(token::Brace) { - // let inner; - // syn::braced!(inner in outer); - // let styles: Styles = inner.parse()?; - // MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site())) - // } else { - // just parse as an expression - outer.parse()? - // } + let fork = s.fork(); + if let Ok(rawtext) = fork.parse::() { + s.advance_to(&fork); + AttrType::Value(rawtext) } else { - s.parse()? - }; - AttrType::Value(lit_str) + let toks = s.parse::()?; + AttrType::FieldTokens(toks) + } + // let lit_str = if name_str == "style" && s.peek(token::Brace) { + // // special-case to deal with literal styles. + // let outer; + // syn::braced!(outer in s); + // // double brace for inline style. + // // todo!("Style support not ready yet"); + + // // if outer.peek(token::Brace) { + // // let inner; + // // syn::braced!(inner in outer); + // // let styles: Styles = inner.parse()?; + // // MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site())) + // // } else { + // // just parse as an expression + // outer.parse()? + // // } + // } else { + // s.parse()? + // }; }; // consume comma if it exists @@ -423,11 +426,11 @@ impl Parse for Attr { let _ = s.parse::(); } - Ok(Attr { name, ty }) + Ok(ElementAttr { name, ty }) } } -impl ToTokens for &Attr { +impl ToTokens for &ElementAttr { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = self.name.to_string(); let nameident = &self.name; @@ -435,7 +438,12 @@ impl ToTokens for &Attr { match &self.ty { AttrType::Value(value) => { tokens.append_all(quote! { - .attr(#name, #value) + .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() + }) }); } AttrType::Event(event) => { @@ -443,21 +451,21 @@ impl ToTokens for &Attr { .add_listener(dioxus::events::on::#nameident(ctx, #event)) }); } - AttrType::Tok(exp) => { + AttrType::FieldTokens(exp) => { tokens.append_all(quote! { - .add_listener(dioxus::events::on::#nameident(ctx, #exp)) + .attr(#name, #exp) }); } + AttrType::EventTokens(event) => { + // + tokens.append_all(quote! { + .add_listener(dioxus::events::on::#nameident(ctx, #event)) + }) + } } } } -enum AttrType { - Value(LitStr), - Event(ExprClosure), - Tok(Expr), -} - // ======================================= // Parse just plain text // ======================================= @@ -483,3 +491,9 @@ impl ToTokens for TextNode { }); } } + +fn try_parse_bracketed(stream: &ParseBuffer) -> Result { + let content; + syn::braced!(content in stream); + content.parse() +} diff --git a/packages/core-macro/src/util.rs b/packages/core-macro/src/util.rs index f3d26e1b..8e1f354d 100644 --- a/packages/core-macro/src/util.rs +++ b/packages/core-macro/src/util.rs @@ -117,6 +117,9 @@ static VALID_TAGS: Lazy> = Lazy::new(|| { "var", "video", "wbr", + // SVTG + "svg", + "path", ] .iter() .cloned() @@ -132,6 +135,6 @@ static VALID_TAGS: Lazy> = Lazy::new(|| { /// /// assert_eq!(is_valid_tag("random"), false); /// ``` -pub fn is_valid_tag(tag: &str) -> bool { +pub fn is_valid_html_tag(tag: &str) -> bool { VALID_TAGS.contains(tag) } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 39787b78..28dc56f2 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -34,4 +34,4 @@ log = "0.4.14" serde = { version = "1.0.123", features = ["derive"], optional = true } [features] -default = [] +default = ["serde"] diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 56a1c3a6..dd85d267 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -7,7 +7,7 @@ fn main() {} -use std::borrow::Borrow; +use std::{borrow::Borrow, rc::Rc}; use dioxus_core::prelude::*; @@ -36,7 +36,7 @@ fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { // create the props with nothing but the fc fc_to_builder(ChildItem) .item(child) - .item_handler(set_val) + .item_handler(set_val.clone()) .build(), None, )); @@ -52,7 +52,7 @@ struct ChildProps<'a> { item: &'a ListItem, // Even pass down handlers! - item_handler: &'a dyn Fn(i32), + item_handler: Rc, } impl PartialEq for ChildProps<'_> { diff --git a/packages/core/examples/dummy.rs b/packages/core/examples/dummy.rs deleted file mode 100644 index 8f6c30a8..00000000 --- a/packages/core/examples/dummy.rs +++ /dev/null @@ -1,102 +0,0 @@ -// #![allow(unused, non_upper_case_globals)] -// use bumpalo::Bump; -// use dioxus_core::nodebuilder::*; -// use dioxus_core::prelude::VNode; -// use dioxus_core::prelude::*; -// use once_cell::sync::{Lazy, OnceCell}; - -use std::ops::Deref; - -/* -A guard over underlying T that provides access in callbacks via "Copy" -*/ - -// #[derive(Clone)] -struct ContextGuard2 { - _val: std::marker::PhantomData, -} -impl Clone for ContextGuard2 { - // we aren't cloning the underlying data so clone isn't necessary - fn clone(&self) -> Self { - todo!() - } -} -impl Copy for ContextGuard2 {} - -impl ContextGuard2 { - fn get<'a>(&'a self) -> ContextLock<'a, T> { - todo!() - } -} - -struct ContextLock<'a, T> { - _val: std::marker::PhantomData<&'a T>, -} -impl<'a, T: 'a + 'static> Deref for ContextLock<'a, T> { - type Target = T; - - fn deref<'b>(&'b self) -> &'b T { - todo!() - } -} - -/* -The source of the data that gives out context guards -*/ -struct Context<'a> { - _p: std::marker::PhantomData<&'a ()>, -} - -impl<'a> Context<'a> { - fn use_context<'b, I, O: 'b>(&self, _f: fn(&'b I) -> O) -> ContextGuard2 { - todo!() - } - fn add_listener(&self, _f: impl Fn(()) + 'a) { - todo!() - } - - fn render(self, _f: impl FnOnce(&'a String) + 'a) {} - // fn view(self, f: impl for<'b> FnOnce(&'a String) + 'a) {} - // fn view(self, f: impl for<'b> FnOnce(&'b String) + 'a) {} -} - -struct Example { - value: String, -} -/* -Example compiling -*/ -fn t<'a>(ctx: Context<'a>) { - let value = ctx.use_context(|b: &Example| &b.value); - - // Works properly, value is moved by copy into the closure - let refed = value.get(); - println!("Value is {}", refed.as_str()); - let r2 = refed.as_str(); - - ctx.add_listener(move |_| { - // let val = value.get().as_str(); - let _val2 = r2.as_bytes(); - println!("v2 is {}", r2); - // println!("refed is {}", refed); - }); - - // let refed = value.deref(); - // returns &String - - // returns &String - // let refed = value.deref(); // returns &String - // let refed = value.deref(); // returns &String - - // Why does this work? This closure should be static but is holding a reference to refed - // The context guard is meant to prevent any references moving into the closure - // if the references move they might become invalid due to mutlithreading issues - ctx.add_listener(move |_| { - // let val = value.as_str(); - // let val2 = refed.as_bytes(); - }); - - ctx.render(move |_b| {}); -} - -fn main() {} diff --git a/packages/core/examples/fmter.rs b/packages/core/examples/fmter.rs deleted file mode 100644 index 0136757b..00000000 --- a/packages/core/examples/fmter.rs +++ /dev/null @@ -1,63 +0,0 @@ -// #[macro_use] - -// use dioxus_core::ifmt; -// use fstrings::format_args_f; - -fn main() { - let bump = bumpalo::Bump::new(); - let _b = ≎ - let _world = "123"; - // dioxus_core::ifmt!(in b; "Hello {world}";); -} - -// let mut s = bumpalo::collections::String::new_in(b); -// fstrings::write_f!(s, "Hello {world}"); -// dbg!(s); -// let p = { -// println!("hello, {}", &world); -// () -// }; -// let g = format_args!("hello {world}", world = world); -// let g = dioxus_core::ifmt!(in b, "Hello {world}"); -// let g = ifmt!(in b, "hhello {world}"); -// let g = ::core::fmt::Arguments::new_v1( -// &["hello "], -// &match (&world,) { -// (arg0,) => [::core::fmt::ArgumentV1::new( -// arg0, -// ::core::fmt::Display::fmt, -// )], -// }, -// ); -// fn main() { -// let bump = bumpalo::Bump::new(); -// let b = ≎ -// let world = "123"; -// let world = 123; -// let g = { -// use bumpalo::core_alloc::fmt::Write; -// use ::dioxus_core::prelude::bumpalo; -// use ::dioxus_core::prelude::format_args_f; -// let bump = b; -// let mut s = bumpalo::collections::String::new_in(bump); -// let _ = (&mut s).write_fmt(::core::fmt::Arguments::new_v1( -// &[""], -// &match (&::core::fmt::Arguments::new_v1( -// &["hhello "], -// &match (&world,) { -// (arg0,) => [::core::fmt::ArgumentV1::new( -// arg0, -// ::core::fmt::Display::fmt, -// )], -// }, -// ),) -// { -// (arg0,) => [::core::fmt::ArgumentV1::new( -// arg0, -// ::core::fmt::Display::fmt, -// )], -// }, -// )); -// s -// }; -// } diff --git a/packages/core/examples/macro_testing.rs b/packages/core/examples/macro_testing.rs new file mode 100644 index 00000000..804fc768 --- /dev/null +++ b/packages/core/examples/macro_testing.rs @@ -0,0 +1,54 @@ +use baller::Baller; +use dioxus_core::prelude::*; + +fn main() { + let g = rsx! { + div { + crate::baller::Baller {} + baller::Baller { + } + Taller { + a: "asd" + } + baller::Baller {} + baller::Baller {} + Baller {} + div { + a: "asd", + a: "asd", + a: "asd", + a: "asd", + div { + "asdas", + "asdas", + "asdas", + "asdas", + div { + + }, + div { + + }, + } + } + } + }; +} + +mod baller { + use super::*; + pub struct BallerProps {} + + pub fn Baller(ctx: Context, props: &()) -> DomTree { + todo!() + } +} + +#[derive(Debug, PartialEq, Props)] +pub struct TallerProps { + a: &'static str, +} + +pub fn Taller(ctx: Context, props: &TallerProps) -> DomTree { + todo!() +} diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index caa5af54..efe46481 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -1,4 +1,4 @@ -use crate::{nodebuilder::IntoDomTree, prelude::*, scope::Scope}; +use crate::{innerlude::*, nodebuilder::IntoDomTree}; use crate::{nodebuilder::LazyNodes, nodes::VNode}; use bumpalo::Bump; use hooks::Hook; @@ -97,9 +97,12 @@ impl<'a> Context<'a> { &self, lazy_nodes: LazyNodes<'a, F>, ) -> DomTree { + // let idx = self.idx.borrow(); let ctx = NodeCtx { bump: &self.scope.cur_frame().bump, scope: self.scope.myidx, + + // hmmmmmmmm not sure if this is right idx: 0.into(), listeners: &self.scope.listeners, }; @@ -151,11 +154,14 @@ pub mod hooks { let new_state = initializer(); hooks.push(Hook::new(new_state)); } - *self.idx.borrow_mut() = 1; + + *self.idx.borrow_mut() += 1; let stable_ref = hooks.get_mut(idx).unwrap().0.as_mut(); let v = unsafe { Pin::get_unchecked_mut(stable_ref) }; - let internal_state = v.downcast_mut::().unwrap(); + let internal_state = v + .downcast_mut::() + .expect("couldn't find the hook state"); // we extend the lifetime from borrowed in this scope to borrowed from self. // This is okay because the hook is pinned diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index ed977bc6..f7cdebe3 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -32,7 +32,7 @@ //! //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ -use crate::{innerlude::*, scope::Scope}; +use crate::innerlude::*; use bumpalo::Bump; use fxhash::{FxHashMap, FxHashSet}; use generational_arena::Arena; @@ -66,26 +66,29 @@ pub struct DiffMachine<'a> { pub enum LifeCycleEvent<'a> { Mount { caller: Weak Fn(Context<'r>) -> DomTree + 'a>, - scope: Weak, - id: u32, + stable_scope_addr: Weak, + root_id: u32, }, PropsChanged { caller: Weak Fn(Context<'r>) -> DomTree + 'a>, - scope: Weak, - id: u32, + stable_scope_addr: Weak, + root_id: u32, }, SameProps { caller: Weak Fn(Context<'r>) -> DomTree + 'a>, - scope: Weak, - id: u32, + stable_scope_addr: Weak, + root_id: u32, }, Replace { caller: Weak Fn(Context<'r>) -> DomTree + 'a>, old_scope: Weak, new_scope: Weak, - id: u32, + root_id: u32, + }, + Remove { + stable_scope_addr: Weak, + root_id: u32, }, - Remove, } static COUNTER: AtomicU32 = AtomicU32::new(1); @@ -159,7 +162,11 @@ impl<'a> DiffMachine<'a> { let scope = Rc::downgrade(&cold.ass_scope); self.lifecycle_events - .push_back(LifeCycleEvent::PropsChanged { caller, id, scope }); + .push_back(LifeCycleEvent::PropsChanged { + caller, + root_id: id, + stable_scope_addr: scope, + }); } else { let caller = Rc::downgrade(&cnew.caller); let id = cold.stable_addr.borrow().unwrap(); @@ -168,7 +175,7 @@ impl<'a> DiffMachine<'a> { self.lifecycle_events.push_back(LifeCycleEvent::Replace { caller, - id, + root_id: id, old_scope, new_scope, }); @@ -262,8 +269,8 @@ impl<'a> DiffMachine<'a> { let scope = Rc::downgrade(&component.ass_scope); self.lifecycle_events.push_back(LifeCycleEvent::Mount { caller: Rc::downgrade(&component.caller), - id, - scope, + root_id: id, + stable_scope_addr: scope, }); } VNode::Suspended => { diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 3ba5333e..4afc3a62 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -21,7 +21,7 @@ mod use_state_def { struct UseState { new_val: Rc>>, current_val: T, - caller: Box, + caller: Rc, } /// Store state between component renders! @@ -49,12 +49,12 @@ mod use_state_def { pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>( ctx: &'c Context<'a>, initial_state_fn: F, - ) -> (&'a T, &'a impl Fn(T)) { + ) -> (&'a T, &'a Rc) { ctx.use_hook( move || UseState { new_val: Rc::new(RefCell::new(None)), current_val: initial_state_fn(), - caller: Box::new(|_| println!("setter called!")), + caller: Rc::new(|_| println!("setter called!")), }, move |hook| { log::debug!("Use_state set called"); @@ -69,7 +69,7 @@ mod use_state_def { } // todo: swap out the caller with a subscription call and an internal update - hook.caller = Box::new(move |new_val| { + hook.caller = Rc::new(move |new_val| { // update the setter with the new value let mut new_inner = inner.as_ref().borrow_mut(); *new_inner = Some(new_val); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 1cdc26f6..85479ea9 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -68,18 +68,14 @@ pub mod component; // Logic for extending FC pub mod context; // Logic for providing hook + context functionality to user components pub mod debug_renderer; -pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately - // pub mod diff; - // pub mod patch; // The diffing algorithm that builds the ChangeList pub mod diff; -// the diffing algorithm that builds the ChangeList +pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately + // the diffing algorithm that builds the ChangeList pub mod error; // Error type we expose to the renderers pub mod events; // Manages the synthetic event API pub mod hooks; // Built-in hooks pub mod nodebuilder; // Logic for building VNodes with a direct syntax pub mod nodes; // Logic for the VNodes -pub mod scope; // Logic for single components - // pub mod validation; // Logic for validating trees pub mod virtual_dom; // Most fun logic starts here, manages the lifecycle and suspense pub mod builder { @@ -88,37 +84,20 @@ pub mod builder { // types used internally that are important pub(crate) mod innerlude { - // pub(crate) use crate::component::Properties; - pub(crate) use crate::component::Properties; - pub(crate) use crate::context::Context; - pub use crate::diff::LifeCycleEvent; - pub(crate) use crate::error::Result; - pub use crate::events::{EventTrigger, VirtualEvent}; - use crate::nodes; - - pub use crate::component::ScopeIdx; - pub use crate::context::hooks::Hook; - pub use crate::nodes::VNode; - - pub(crate) use nodes::*; - - pub use crate::context::NodeCtx; - pub use crate::diff::DiffMachine; - pub use crate::patch::{EditList, EditMachine}; - // pub use crate::patchdx; - // pub use crate::patchtList; - // pub use nodes::iterables::IterableNodes; - /// This type alias is an internal way of abstracting over the static functions that represent components. + pub use crate::component::*; + pub use crate::context::*; + pub use crate::debug_renderer::*; + pub use crate::diff::*; + pub use crate::error::*; + pub use crate::events::*; + pub use crate::hooks::*; + pub use crate::nodebuilder::*; + pub use crate::nodes::*; + pub use crate::patch::*; + pub use crate::virtual_dom::*; pub type FC

= for<'scope> fn(Context<'scope>, &'scope P) -> DomTree; - mod fc2 {} - // pub type FC<'a, P: 'a> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree; - // pub type FC

= for<'scope, 'r> fn(Context<'scope>, &'scope P) -> DomTree; - // pub type FC

= for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>; - // pub type FC

= for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>; - // pub type FC

= for<'a> fn(Context<'a, P>) -> VNode<'a>; - // TODO @Jon, fix this // hack the VNode type until VirtualNode is fixed in the macro crate pub type VirtualNode<'a> = VNode<'a>; @@ -127,7 +106,6 @@ pub(crate) mod innerlude { pub use crate as dioxus; pub use crate::nodebuilder as builder; pub use dioxus_core_macro::{html, rsx}; - // pub use dioxus_core_macro::{fc, html, rsx}; } /// Re-export common types for ease of development use. diff --git a/packages/core/src/patch.rs b/packages/core/src/patch.rs index 306ce8e5..f06ab788 100644 --- a/packages/core/src/patch.rs +++ b/packages/core/src/patch.rs @@ -21,9 +21,14 @@ //! ---- //! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom) +use std::cell::Ref; + use crate::innerlude::ScopeIdx; pub type EditList<'src> = Vec>; +// pub struct EditList<'src> { +// edits: Vec>, +// } /// The `Edit` represents a single modifcation of the renderer tree. /// todo @jon, go through and make certain fields static. tag names should be known at compile time @@ -146,7 +151,7 @@ pub struct EditMachine<'lock> { pub traversal: Traversal, next_temporary: u32, forcing_new_listeners: bool, - + // // if the current node is a "known" node // // any actions that modify this node should update the mapping // current_known: Option, @@ -338,7 +343,7 @@ impl<'a> EditMachine<'a> { } pub fn create_element(&mut self, tag_name: &'a str) { - debug_assert!(self.traversal_is_committed());; + debug_assert!(self.traversal_is_committed()); self.emitter.push(Edit::CreateElement { tag_name }); } @@ -389,7 +394,6 @@ impl<'a> EditMachine<'a> { // self.current_known = Some(id); self.emitter.push(Edit::TraverseToKnown { node: id }) } - } // Keeps track of where we are moving in a DOM tree, and shortens traversal diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs deleted file mode 100644 index 821e3d9c..00000000 --- a/packages/core/src/scope.rs +++ /dev/null @@ -1,382 +0,0 @@ -use crate::{innerlude::*, virtual_dom::OpaqueComponent}; -use bumpalo::Bump; - -use std::{ - cell::RefCell, - collections::HashSet, - marker::PhantomData, - rc::{Rc, Weak}, -}; - -/// Every component in Dioxus is represented by a `Scope`. -/// -/// Scopes contain the state for hooks, the component's props, and other lifecycle information. -/// -/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components. -/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. -pub struct Scope { - // Map to the parent - pub parent: Option, - - // our own index - pub myidx: ScopeIdx, - - // - pub children: HashSet, - - pub caller: Weak>, - - // ========================== - // slightly unsafe stuff - // ========================== - // an internal, highly efficient storage of vnodes - pub frames: ActiveFrame, - - // These hooks are actually references into the hook arena - // These two could be combined with "OwningRef" to remove unsafe usage - // or we could dedicate a tiny bump arena just for them - // could also use ourborous - pub hooks: RefCell>, - - pub hook_arena: Vec, - - // Unsafety: - // - is self-refenrential and therefore needs to point into the bump - // Stores references into the listeners attached to the vnodes - // NEEDS TO BE PRIVATE - pub(crate) listeners: RefCell>, -} - -impl Scope { - // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) - // we are going to break this lifetime by force in order to save it on ourselves. - // To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies. - // This should never happen, but is a good check to keep around - pub fn new<'creator_node>( - caller: Weak>, - myidx: ScopeIdx, - parent: Option, - ) -> Self { - // Caller has been broken free - // However, it's still weak, so if the original Rc gets killed, we can't touch it - let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; - - Self { - caller: broken_caller, - hook_arena: Vec::new(), - hooks: RefCell::new(Vec::new()), - frames: ActiveFrame::new(), - children: HashSet::new(), - listeners: Default::default(), - parent, - myidx, - } - } - - pub fn update_caller<'creator_node>(&mut self, caller: Weak>) { - let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; - - self.caller = broken_caller; - } - - /// Create a new context and run the component with references from the Virtual Dom - /// This function downcasts the function pointer based on the stored props_type - /// - /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) - pub fn run_scope<'b>(&'b mut self) -> Result<()> { - // cycle to the next frame and then reset it - // this breaks any latent references - self.frames.next().bump.reset(); - - let ctx = Context { - idx: 0.into(), - _p: PhantomData {}, - scope: self, - }; - - let caller = self.caller.upgrade().expect("Failed to get caller"); - - /* - SAFETY ALERT - - DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS. - KEEPING THIS STATIC REFERENCE CAN LEAD TO UB. - - Some things to note: - - The VNode itself is bound to the lifetime, but it itself is owned by scope. - - The VNode has a private API and can only be used from accessors. - - Public API cannot drop or destructure VNode - */ - let new_head = unsafe { - // use the same type, just manipulate the lifetime - type ComComp<'c> = Rc>; - let caller = std::mem::transmute::, ComComp<'b>>(caller); - (caller.as_ref())(ctx) - }; - - self.frames.cur_frame_mut().head_node = new_head.root; - Ok(()) - } - - // A safe wrapper around calling listeners - // calling listeners will invalidate the list of listeners - // The listener list will be completely drained because the next frame will write over previous listeners - pub fn call_listener(&mut self, trigger: EventTrigger) { - let EventTrigger { - listener_id, - event: source, - .. - } = trigger; - - unsafe { - let listener = self - .listeners - .borrow() - .get(listener_id as usize) - .expect("Listener should exist if it was triggered") - .as_ref() - .unwrap(); - - // Run the callback with the user event - log::debug!("Running listener"); - listener(source); - log::debug!("Listener finished"); - - // 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.borrow_mut().drain(..); - } - } - - pub fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { - self.frames.current_head_node() - } - - pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { - self.frames.prev_head_node() - } - - pub fn cur_frame(&self) -> &BumpFrame { - self.frames.cur_frame() - } -} - -// ========================== -// Active-frame related code -// ========================== -// todo, do better with the active frame stuff -// somehow build this vnode with a lifetime tied to self -// This root node has "static" lifetime, but it's really not static. -// It's goverened by the oldest of the two frames and is switched every time a new render occurs -// Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref -// ! do not copy this reference are things WILL break ! -pub struct ActiveFrame { - pub idx: RefCell, - pub frames: [BumpFrame; 2], -} - -pub struct BumpFrame { - pub bump: Bump, - pub head_node: VNode<'static>, -} - -impl ActiveFrame { - pub fn new() -> Self { - Self::from_frames( - BumpFrame { - bump: Bump::new(), - head_node: VNode::text(""), - }, - BumpFrame { - bump: Bump::new(), - head_node: VNode::text(""), - }, - ) - } - - fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { - Self { - idx: 0.into(), - frames: [a, b], - } - } - - fn cur_frame(&self) -> &BumpFrame { - match *self.idx.borrow() & 1 == 0 { - true => &self.frames[0], - false => &self.frames[1], - } - } - fn cur_frame_mut(&mut self) -> &mut BumpFrame { - match *self.idx.borrow() & 1 == 0 { - true => &mut self.frames[0], - false => &mut self.frames[1], - } - } - - pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.idx.borrow() & 1 == 0 { - true => &self.frames[0], - false => &self.frames[1], - }; - - // Give out our self-referential item with our own borrowed lifetime - unsafe { - let unsafe_head = &raw_node.head_node; - let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); - safe_node - } - } - - pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { - let raw_node = match *self.idx.borrow() & 1 != 0 { - true => &self.frames[0], - false => &self.frames[1], - }; - - // Give out our self-referential item with our own borrowed lifetime - unsafe { - let unsafe_head = &raw_node.head_node; - let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); - safe_node - } - } - - fn next(&mut self) -> &mut BumpFrame { - *self.idx.borrow_mut() += 1; - - if *self.idx.borrow() % 2 == 0 { - &mut self.frames[0] - } else { - &mut self.frames[1] - } - } -} - -#[cfg(old)] -#[cfg(test)] -mod tests { - use super::*; - use crate::prelude::*; - - // static ListenerTest: FC<()> = |ctx, props| { - // ctx.render(html! { - //

- // "hello" - //
- // }) - // }; - - #[test] - fn test_scope() { - #[derive(PartialEq)] - struct Example {} - impl FC for Example { - fn render(ctx: Context<'_>, _: &Self) -> DomTree { - use crate::builder::*; - ctx.render(|ctx| { - builder::ElementBuilder::new(ctx, "div") - .child(text("a")) - .finish() - }) - } - } - - let mut nodes = generational_arena::Arena::new(); - nodes.insert_with(|myidx| { - let scope = create_scoped(Example {}, myidx, None); - }); - } - - use crate::{builder::*, hooks::use_ref}; - - #[derive(Debug, PartialEq)] - struct EmptyProps<'src> { - name: &'src String, - } - - impl FC for EmptyProps<'_> { - fn render(ctx: Context, props: &Self) -> DomTree { - let (content, _): (&String, _) = crate::hooks::use_state(&ctx, || "abcd".to_string()); - - let childprops: ExampleProps<'_> = ExampleProps { name: content }; - todo!() - // ctx.render(move |c| { - // builder::ElementBuilder::new(c, "div") - // .child(text(props.name)) - // .child(virtual_child(c, childprops)) - // .finish() - // }) - } - } - - #[derive(Debug, PartialEq)] - struct ExampleProps<'src> { - name: &'src String, - } - - impl FC for ExampleProps<'_> { - fn render(ctx: Context, props: &Self) -> DomTree { - ctx.render(move |ctx| { - builder::ElementBuilder::new(ctx, "div") - .child(text(props.name)) - .finish() - }) - } - } - - #[test] - fn test_borrowed_scope() { - #[derive(Debug, PartialEq)] - struct Example { - name: String, - } - - impl FC for Example { - fn render(ctx: Context, props: &Self) -> DomTree { - todo!() - // ctx.render(move |c| { - // builder::ElementBuilder::new(c, "div") - // .child(virtual_child(c, ExampleProps { name: &props.name })) - // .finish() - // }) - } - } - - let source_text = "abcd123".to_string(); - let props = ExampleProps { name: &source_text }; - } -} - -#[cfg(asd)] -mod old { - - /// The ComponentCaller struct is an opaque object that encapsultes the memoization and running functionality for FC - /// - /// It's opaque because during the diffing mechanism, the type of props is sealed away in a closure. This makes it so - /// scope doesn't need to be generic - pub struct ComponentCaller { - // used as a memoization strategy - comparator: Box) -> bool>, - - // used to actually run the component - // encapsulates props - runner: Box DomTree>, - - props_type: TypeId, - - // the actual FC - raw: *const (), - } - - impl ComponentCaller { - fn new

(props: P) -> Self { - let comparator = Box::new(|f| false); - todo!(); - // Self { comparator } - } - - fn update_props

(props: P) {} - } -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 091f4731..84fc2926 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,13 +1,12 @@ -// use crate::{changelist::EditList, nodes::VNode}; - use crate::{error::Error, innerlude::*}; -use crate::{patch::Edit, scope::Scope}; +use crate::{innerlude::hooks::Hook, patch::Edit}; +use bumpalo::Bump; use generational_arena::Arena; use std::{ any::TypeId, borrow::{Borrow, BorrowMut}, - cell::RefCell, - collections::{BTreeMap, BTreeSet}, + cell::{Ref, RefCell, UnsafeCell}, + collections::{BTreeMap, BTreeSet, BinaryHeap, HashSet}, rc::{Rc, Weak}, }; use thiserror::private::AsDynError; @@ -21,23 +20,30 @@ pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + ' pub struct VirtualDom { /// All mounted components are arena allocated to make additions, removals, and references easy to work with /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena. - pub components: Arena, + /// + /// This is wrapped in an UnsafeCell because we will need to get mutable access to unique values in unique bump arenas + /// and rusts's guartnees cannot prove that this is safe. We will need to maintain the safety guarantees manually. + components: UnsafeCell>, - /// The index of the root component. - /// Will not be ready if the dom is fresh + /// The index of the root component.\ + /// Should always be the first pub base_scope: ScopeIdx, + /// All components dump their updates into a queue to be processed pub(crate) update_schedule: UpdateFunnel, // a strong allocation to the "caller" for the original props #[doc(hidden)] - _root_caller: Rc>, + root_caller: Rc>, // Type of the original props. This is done so VirtualDom does not need to be generic. #[doc(hidden)] _root_prop_type: std::any::TypeId, } +// ====================================== +// Public Methods for the VirtualDOM +// ====================================== impl VirtualDom { /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. /// @@ -55,109 +61,50 @@ impl VirtualDom { pub fn new_with_props(root: FC

, root_props: P) -> Self { let mut components = Arena::new(); - // let prr = Rc::new(root_props); + // The user passes in a "root" component (IE the function) + // When components are used in the rsx! syntax, the parent assumes ownership + // Here, the virtual dom needs to own the function, wrapping it with a `context caller` + // The RC holds this component with a hard allocation + let root_caller: Rc = Rc::new(move |ctx| root(ctx, &root_props)); - // the root is kept around with a "hard" allocation - let root_caller: Rc = Rc::new(move |ctx| { - // - // let p2 = &root_props; - // let p2 = prr.clone(); - root(ctx, &root_props) - }); - - // we then expose this to the component with a weak allocation - let weak_caller: Weak = Rc::downgrade(&root_caller); - - let base_scope = components.insert_with(move |myidx| Scope::new(weak_caller, myidx, None)); + // To make it easier to pass the root around, we just leak it + // When the virtualdom is dropped, we unleak it, so that unsafe isn't here, but it's important to remember + let leaked_caller = Rc::downgrade(&root_caller); Self { - components, - _root_caller: root_caller, - base_scope, + root_caller, + base_scope: components + .insert_with(move |myidx| Scope::new(leaked_caller, myidx, None, 0)), + components: UnsafeCell::new(components), update_schedule: UpdateFunnel::default(), _root_prop_type: TypeId::of::

(), } } - // consume the top of the diff machine event cycle and dump edits into the edit list - pub fn step(&mut self, event: LifeCycleEvent) -> Result<()> { - Ok(()) - } - - /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom. + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom. from scratch pub fn rebuild<'s>(&'s mut self) -> Result> { - // Diff from the top - let mut diff_machine = DiffMachine::new(); - - let very_unsafe_components = &mut self.components as *mut generational_arena::Arena; - let component = self - .components - .get_mut(self.base_scope) - .ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?; - component.run_scope()?; - diff_machine.diff_node(component.old_frame(), component.new_frame()); - - // log::debug!("New events generated: {:#?}", diff_machine.lifecycle_events); - // chew down the the lifecycle events until all dirty nodes are computed - while let Some(event) = diff_machine.lifecycle_events.pop_front() { - match event { - // A new component has been computed from the diffing algorithm - // create a new component in the arena, run it, move the diffing machine to this new spot, and then diff it - // this will flood the lifecycle queue with new updates - LifeCycleEvent::Mount { caller, id, scope } => { - log::debug!("Mounting a new component"); - - // We're modifying the component arena while holding onto references into the assoiated bump arenas of its children - // those references are stable, even if the component arena moves around in memory, thanks to the bump arenas. - // However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime. - unsafe { - let p = &mut *(very_unsafe_components); - - // todo, hook up the parent/child indexes properly - let idx = p.insert_with(|f| Scope::new(caller, f, None)); - let c = p.get_mut(idx).unwrap(); - - let real_scope = scope.upgrade().unwrap(); - *real_scope.as_ref().borrow_mut() = Some(idx); - c.run_scope()?; - diff_machine.change_list.load_known_root(id); - diff_machine.diff_node(c.old_frame(), c.new_frame()); - } - } - LifeCycleEvent::PropsChanged { caller, id, scope } => { - log::debug!("PROPS CHANGED"); - let idx = scope.upgrade().unwrap().as_ref().borrow().unwrap(); - unsafe { - let p = &mut *(very_unsafe_components); - let c = p.get_mut(idx).unwrap(); - c.update_caller(caller); - c.run_scope()?; - diff_machine.change_list.load_known_root(id); - diff_machine.diff_node(c.old_frame(), c.new_frame()); - } - // break 'render; - } - LifeCycleEvent::SameProps { caller, id, scope } => { - log::debug!("SAME PROPS RECEIVED"); - // - // break 'render; - } - LifeCycleEvent::Remove => { - // - // break 'render; - } - LifeCycleEvent::Replace { caller, id, .. } => {} - } - - // } else { - // break 'render; - // } - } - - let edits: Vec> = diff_machine.consume(); - Ok(edits) + todo!() } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct HeightMarker { + idx: ScopeIdx, + height: u32, +} +impl Ord for HeightMarker { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.height.cmp(&other.height) + } +} + +impl PartialOrd for HeightMarker { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl VirtualDom { /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. /// /// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual @@ -165,7 +112,16 @@ impl VirtualDom { /// change list. /// /// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on - /// listeners. + /// listeners, something like this: + /// + /// ```ignore + /// while let Ok(event) = receiver.recv().await { + /// let edits = self.internal_dom.progress_with_event(event)?; + /// for edit in &edits { + /// patch_machine.handle_edit(edit); + /// } + /// } + /// ``` /// /// Note: this method is not async and does not provide suspense-like functionality. It is up to the renderer to provide the /// executor and handlers for suspense as show in the example. @@ -182,67 +138,508 @@ impl VirtualDom { /// } /// /// ``` - pub fn progress_with_event(&mut self, event: EventTrigger) -> Result> { - let component = self - .components - .get_mut(event.component_id) - .expect("Borrowing should not fail"); + // + // Developer notes: + // ---- + // This method has some pretty complex safety guarantees to uphold. + // We interact with bump arenas, raw pointers, and use UnsafeCell to get a partial borrow of the arena. + // The final EditList has edits that pull directly from the Bump Arenas which add significant complexity + // in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe + // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. + pub fn progress_with_event(&mut self, event: EventTrigger) -> Result { + let id = event.component_id.clone(); - component.call_listener(event); + unsafe { + (&mut *self.components.get()) + .get_mut(id) + .expect("Borrowing should not fail") + .call_listener(event)?; + } - /* - -> call listener - -> sort through accumulated queue by the top - -> run each component, running its children - -> add new updates to the tree of updates (these are dirty) - -> - */ - // component.run_scope()?; + // Add this component to the list of components that need to be difed + let mut diff_machine = DiffMachine::new(); + let mut cur_height = 0; - // let mut diff_machine = DiffMachine::new(); - // let mut diff_machine = DiffMachine::new(&self.diff_bump); + // Now, there are events in the queue + let mut seen_nodes = HashSet::::new(); + let mut updates = self.update_schedule.0.as_ref().borrow_mut(); - // diff_machine.diff_node(component.old_frame(), component.new_frame()); + // Order the nodes by their height, we want the biggest nodes on the top + // This prevents us from running the same component multiple times + updates.sort_unstable(); - // Ok(diff_machine.consume()) - self.rebuild() + // Iterate through the triggered nodes (sorted by height) and begin to diff them + for update in updates.drain(..) { + // Make sure this isn't a node we've already seen, we don't want to double-render anything + // If we double-renderer something, this would cause memory safety issues + if seen_nodes.contains(&update.idx) { + continue; + } + + // Now, all the "seen nodes" are nodes that got notified by running this listener + seen_nodes.insert(update.idx.clone()); + + unsafe { + // Start a new mutable borrow to components + + // We are guaranteeed that this scope is unique because we are tracking which nodes have modified + let component = (&mut *self.components.get()) + .get_mut(update.idx) + .expect("msg"); + + component.run_scope()?; + + diff_machine.diff_node(component.old_frame(), component.next_frame()); + + cur_height = component.height + 1; + } + + // Now, the entire subtree has been invalidated. We need to descend depth-first and process + // any updates that the diff machine has proprogated into the component lifecycle queue + while let Some(event) = diff_machine.lifecycle_events.pop_front() { + match event { + // A new component has been computed from the diffing algorithm + // create a new component in the arena, run it, move the diffing machine to this new spot, and then diff it + // this will flood the lifecycle queue with new updates to build up the subtree + LifeCycleEvent::Mount { + caller, + root_id: id, + stable_scope_addr, + } => { + log::debug!("Mounting a new component"); + + // We're modifying the component arena while holding onto references into the assoiated bump arenas of its children + // those references are stable, even if the component arena moves around in memory, thanks to the bump arenas. + // However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime. + let components: &mut _ = unsafe { &mut *self.components.get() }; + + // Insert a new scope into our component list + let idx = + components.insert_with(|f| Scope::new(caller, f, None, cur_height)); + + // Grab out that component + let component = components.get_mut(idx).unwrap(); + + // Actually initialize the caller's slot with the right address + *stable_scope_addr.upgrade().unwrap().as_ref().borrow_mut() = Some(idx); + + // Run the scope for one iteration to initialize it + component.run_scope()?; + + // Navigate the diff machine to the right point in the output dom + diff_machine.change_list.load_known_root(id); + + // And then run the diff algorithm + diff_machine.diff_node(component.old_frame(), component.next_frame()); + + // Finally, insert this node as a seen node. + seen_nodes.insert(idx); + } + + // A component has remained in the same location but its properties have changed + // We need to process this component and then dump the output lifecycle events into the queue + LifeCycleEvent::PropsChanged { + caller, + root_id, + stable_scope_addr, + } => { + log::debug!("Updating a component after its props have changed"); + + let components: &mut _ = unsafe { &mut *self.components.get() }; + + // Get the stable index to the target component + // This *should* exist due to guarantees in the diff algorithm + let idx = stable_scope_addr + .upgrade() + .unwrap() + .as_ref() + .borrow() + .unwrap(); + + // Grab out that component + let component = components.get_mut(idx).unwrap(); + + // We have to move the caller over or running the scope will fail + component.update_caller(caller); + + // Run the scope + component.run_scope()?; + + // Navigate the diff machine to the right point in the output dom + diff_machine.change_list.load_known_root(root_id); + + // And then run the diff algorithm + diff_machine.diff_node(component.old_frame(), component.next_frame()); + + // Finally, insert this node as a seen node. + seen_nodes.insert(idx); + } + + // A component's parent has updated, but its properties did not change. + // This means the caller ptr is invalidated and needs to be updated, but the component itself does not need to be re-ran + LifeCycleEvent::SameProps { + caller, + root_id, + stable_scope_addr, + } => { + // In this case, the parent made a new DomTree that resulted in the same props for us + // However, since our caller is located in a Bump frame, we need to update the caller pointer (which is now invalid) + log::debug!("Received the same props"); + + let components: &mut _ = unsafe { &mut *self.components.get() }; + + // Get the stable index to the target component + // This *should* exist due to guarantees in the diff algorithm + let idx = stable_scope_addr + .upgrade() + .unwrap() + .as_ref() + .borrow() + .unwrap(); + + // Grab out that component + let component = components.get_mut(idx).unwrap(); + + // We have to move the caller over or running the scope will fail + component.update_caller(caller); + + // This time, we will not add it to our seen nodes since we did not actually run it + } + + LifeCycleEvent::Remove { + root_id, + stable_scope_addr, + } => { + unimplemented!("This feature (Remove) is unimplemented") + } + LifeCycleEvent::Replace { + caller, + root_id: id, + .. + } => { + unimplemented!("This feature (Replace) is unimplemented") + } + } + } + } + + Ok(diff_machine.consume()) } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HierarchyMarker { - source: ScopeIdx, -} +impl Drop for VirtualDom { + fn drop(&mut self) { + // Drop all the components first + // self.components.drain(); -#[derive(Debug, Default, PartialEq, Clone)] -pub struct UpdateFunnel(Rc>>); + // Finally, drop the root caller + unsafe { + // let root: Box> = + // Box::from_raw(self.root_caller as *const OpaqueComponent<'static> as *mut _); -impl UpdateFunnel { - fn schedule_update(&self, source: ScopeIdx) -> impl Fn() { - let inner = self.clone(); - move || { - inner - .0 - .as_ref() - .borrow_mut() - .push(HierarchyMarker { source }) + // std::mem::drop(root); } } } -macro_rules! UpdateFunnel { - (root: $root:expr) => { - VirtualDom::new($root) - }; +#[derive(Debug, Default, Clone)] +pub struct UpdateFunnel(Rc>>); + +impl UpdateFunnel { + fn schedule_update(&self, source: &Scope) -> impl Fn() { + let inner = self.clone(); + let marker = HeightMarker { + height: source.height, + idx: source.myidx, + }; + move || inner.0.as_ref().borrow_mut().push(marker) + } +} +/// Every component in Dioxus is represented by a `Scope`. +/// +/// Scopes contain the state for hooks, the component's props, and other lifecycle information. +/// +/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components. +/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. +pub struct Scope { + // The parent's scope ID + pub parent: Option, + + // Our own ID accessible from the component map + pub myidx: ScopeIdx, + + pub height: u32, + + // A list of children + // TODO, repalce the hashset with a faster hash function + pub children: HashSet, + + // caller: &'static OpaqueComponent<'static>, + pub caller: Weak>, + + // ========================== + // slightly unsafe stuff + // ========================== + // an internal, highly efficient storage of vnodes + pub(crate) frames: ActiveFrame, + + // These hooks are actually references into the hook arena + // These two could be combined with "OwningRef" to remove unsafe usage + // or we could dedicate a tiny bump arena just for them + // could also use ourborous + pub(crate) hooks: RefCell>, + + // Unsafety: + // - is self-refenrential and therefore needs to point into the bump + // Stores references into the listeners attached to the vnodes + // NEEDS TO BE PRIVATE + pub(crate) listeners: RefCell>, } -// #[test] -// fn test_new_vdom() { -// let dom = UpdateFunnel! { -// root: |ctx, props| { -// ctx.render(rsx!{ +impl Scope { + // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) + // we are going to break this lifetime by force in order to save it on ourselves. + // To make sure that the lifetime isn't truly broken, we receive a Weak RC so we can't keep it around after the parent dies. + // This should never happen, but is a good check to keep around + // + // Scopes cannot be made anywhere else except for this file + // Therefore, their lifetimes are connected exclusively to the virtual dom + fn new<'creator_node>( + caller: Weak>, + myidx: ScopeIdx, + parent: Option, + height: u32, + ) -> Self { + log::debug!( + "New scope created, height is {}, idx is {:?}", + height, + myidx + ); + // Caller has been broken free + // However, it's still weak, so if the original Rc gets killed, we can't touch it + let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; -// }) -// } -// }; -// } + Self { + caller: broken_caller, + hooks: RefCell::new(Vec::new()), + frames: ActiveFrame::new(), + children: HashSet::new(), + listeners: Default::default(), + parent, + myidx, + height, + } + } + pub fn update_caller<'creator_node>(&mut self, caller: Weak>) { + let broken_caller: Weak> = unsafe { std::mem::transmute(caller) }; + + self.caller = broken_caller; + } + + /// Create a new context and run the component with references from the Virtual Dom + /// This function downcasts the function pointer based on the stored props_type + /// + /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) + pub fn run_scope<'b>(&'b mut self) -> Result<()> { + // cycle to the next frame and then reset it + // this breaks any latent references + self.frames.next().bump.reset(); + + let ctx = Context { + idx: 0.into(), + _p: std::marker::PhantomData {}, + scope: self, + }; + + let caller = self.caller.upgrade().expect("Failed to get caller"); + + /* + SAFETY ALERT + + DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS. + KEEPING THIS STATIC REFERENCE CAN LEAD TO UB. + + Some things to note: + - The VNode itself is bound to the lifetime, but it itself is owned by scope. + - The VNode has a private API and can only be used from accessors. + - Public API cannot drop or destructure VNode + */ + let new_head = unsafe { + // use the same type, just manipulate the lifetime + type ComComp<'c> = Rc>; + let caller = std::mem::transmute::, ComComp<'b>>(caller); + (caller.as_ref())(ctx) + }; + + self.frames.cur_frame_mut().head_node = new_head.root; + Ok(()) + } + + // A safe wrapper around calling listeners + // calling listeners will invalidate the list of listeners + // The listener list will be completely drained because the next frame will write over previous listeners + pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { + let EventTrigger { + listener_id, + event: source, + .. + } = trigger; + + unsafe { + // Convert the raw ptr into an actual object + // This operation is assumed to be safe + + log::debug!("Running listener"); + + self.listeners + .borrow() + .get(listener_id as usize) + .ok_or(Error::FatalInternal("Event should exist if it was triggered"))? + .as_ref() + .ok_or(Error::FatalInternal("Raw event ptr is invalid"))? + // Run the callback with the user event + (source); + + log::debug!("Listener finished"); + + // 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.borrow_mut().drain(..); + } + Ok(()) + } + + pub fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { + self.frames.current_head_node() + } + + pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { + self.frames.prev_head_node() + } + + pub fn cur_frame(&self) -> &BumpFrame { + self.frames.cur_frame() + } +} + +// ========================== +// Active-frame related code +// ========================== +// todo, do better with the active frame stuff +// somehow build this vnode with a lifetime tied to self +// This root node has "static" lifetime, but it's really not static. +// It's goverened by the oldest of the two frames and is switched every time a new render occurs +// Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref +// ! do not copy this reference are things WILL break ! +pub struct ActiveFrame { + pub idx: RefCell, + pub frames: [BumpFrame; 2], + + // We use a "generation" for users of contents in the bump frames to ensure their data isn't broken + pub generation: u32, +} + +pub struct BumpFrame { + pub bump: Bump, + pub head_node: VNode<'static>, +} + +impl ActiveFrame { + pub fn new() -> Self { + Self::from_frames( + BumpFrame { + bump: Bump::new(), + head_node: VNode::text(""), + }, + BumpFrame { + bump: Bump::new(), + head_node: VNode::text(""), + }, + ) + } + + fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { + Self { + idx: 0.into(), + frames: [a, b], + generation: 0, + } + } + + fn cur_frame(&self) -> &BumpFrame { + match *self.idx.borrow() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + } + } + fn cur_frame_mut(&mut self) -> &mut BumpFrame { + match *self.idx.borrow() & 1 == 0 { + true => &mut self.frames[0], + false => &mut self.frames[1], + } + } + + pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { + let raw_node = match *self.idx.borrow() & 1 == 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + + // Give out our self-referential item with our own borrowed lifetime + unsafe { + let unsafe_head = &raw_node.head_node; + let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); + safe_node + } + } + + pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { + let raw_node = match *self.idx.borrow() & 1 != 0 { + true => &self.frames[0], + false => &self.frames[1], + }; + + // Give out our self-referential item with our own borrowed lifetime + unsafe { + let unsafe_head = &raw_node.head_node; + let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head); + safe_node + } + } + + fn next(&mut self) -> &mut BumpFrame { + *self.idx.borrow_mut() += 1; + + if *self.idx.borrow() % 2 == 0 { + &mut self.frames[0] + } else { + &mut self.frames[1] + } + } +} + +mod test { + use super::*; + + #[test] + fn simulate() { + let dom = VirtualDom::new(|ctx, props| { + // + ctx.render(rsx! { + div { + + } + }) + }); + // let root = dom.components.get(dom.base_scope).unwrap(); + } + + // // This marker is designed to ensure resources shared from one bump to another are handled properly + // // The underlying T may be already freed if there is an issue with our crate + // pub(crate) struct BumpResource { + // resource: T, + // scope: ScopeIdx, + // gen: u32, + // } +} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 4820d260..12900b23 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,10 +1,119 @@ -// mod hooks; -// pub use hooks::use_context; +use std::collections::HashMap; -// pub mod prelude { -// use dioxus_core::prelude::Context; -// pub fn use_state(ctx: &Context, init: impl Fn() -> T) -> (T, impl Fn(T)) { -// let g = init(); -// (g, |_| {}) -// } -// } +use dioxus_core::prelude::*; + +/* +a form of use_state explicitly for map-style collections (BTreeMap, HashMap, etc). + +Why? +--- +Traditionally, it's possible to use the "use_state" hook for collections in the React world. +Adding a new entry would look something similar to: + +```js +let (map, set_map) = useState({}); +set_map({ ...map, [key]: value }); +``` +The new value then causes the appropriate update when passed into children. + +This is moderately efficient because the fields of the map are moved, but the data itself is not cloned. +However, if you used similar approach with Dioxus: + +```rust +let (map, set_map) = use_state(ctx, || HashMap::new()); +set_map({ + let mut newmap = map.clone(); + newmap.set(key, value); + newmap +}) +``` +Unfortunately, you'd be cloning the entire state every time a value is changed. The obvious solution is to +wrap every element in the HashMap with an Rc. That way, cloning the HashMap is on par with its JS equivalent. + +Fortunately, we can make this operation even more efficient in Dioxus, leveraging the borrow rules of Rust. + +This hook provides a memoized collection, memoized setters, and memoized getters. This particular hook is +extremely powerful for implementing lists and supporting core state management needs for small apps. + +If you need something even more powerful, check out the dedicated atomic state management Dioxus Dataflow, which +uses the same memoization on top of the use_context API. + +Here's a fully-functional todo app using the use_map API: +```rust +static TodoList: FC<()> = |ctx, props| { + let todos = use_map(ctx, || HashMap::new()); + let input = use_ref(|| None); + + ctx.render(rsx!{ + div { + button { + "Add todo" + onclick: move |_| { + let new_todo = TodoItem::new(input.contents()); + todos.insert(new_todo.id.clone(), new_todo); + input.clear(); + } + } + button { + "Clear todos" + onclick: move |_| todos.clear() + } + input { + placeholder: "What needs to be done?" + ref: input + } + ul { + {todos.iter().map(|todo| rsx!( + li { + key: todo.id + span { "{todo.content}" } + button {"x", onclick: move |_| todos.remove(todo.key.clone())} + } + ))} + } + } + }) +} + +``` + +*/ +fn use_map() {} + +// a form of "use_state" that allows collection memoization +// Elements are received as Rc in case the underlying collection is shuffled around +// Setters/getters can be generated +fn use_collection<'a, T: Collection>( + ctx: &Context<'a>, + f: impl Fn() -> T, +) -> CollectionHandle<'a, T> { + ctx.use_hook( + || {}, + |h| { + // + CollectionHandle { + _p: Default::default(), + } + }, + |h| {}, + ) +} + +struct CollectionMemo {} + +struct CollectionHandle<'a, T: Collection> { + _p: std::marker::PhantomData<&'a T>, +} + +trait Collection {} +impl Collection for std::collections::HashMap {} + +struct MapCollection { + inner: HashMap, +} + +impl MapCollection { + fn set(&self, key: K, val: V) { + // + } +} diff --git a/packages/ios/Cargo.toml b/packages/ios/Cargo.toml new file mode 100644 index 00000000..1bc55d02 --- /dev/null +++ b/packages/ios/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-ios" +version = "0.0.0" +authors = ["Jonathan Kelley "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cacao = { git = "https://github.com/ryanmcgrath/cacao" } +dioxus-core = { path = "../core", version = "0.1.2" } +log = "0.4.14" diff --git a/packages/ios/README.md b/packages/ios/README.md new file mode 100644 index 00000000..a883974d --- /dev/null +++ b/packages/ios/README.md @@ -0,0 +1,3 @@ +# Dioxus for iOS + +This crate implements a renderer of the Dioxus DomTree to an iOS app diff --git a/packages/ios/src/lib.rs b/packages/ios/src/lib.rs new file mode 100644 index 00000000..c4784d59 --- /dev/null +++ b/packages/ios/src/lib.rs @@ -0,0 +1,70 @@ +use dioxus::virtual_dom::VirtualDom; +pub use dioxus_core as dioxus; +use dioxus_core::{events::EventTrigger, prelude::FC}; + +pub struct IosRenderer { + internal_dom: VirtualDom, +} + +impl IosRenderer { + pub async fn run(&mut self) -> dioxus_core::error::Result<()> { + let (sender, mut receiver) = async_channel::unbounded::(); + + // let body_element = prepare_websys_dom(); + + let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| { + log::debug!("Event trigger! {:#?}", ev); + let mut c = sender.clone(); + // wasm_bindgen_futures::spawn_local(async move { + // c.send(ev).await.unwrap(); + // }); + }); + let root_node = body_element.first_child().unwrap(); + patch_machine.stack.push(root_node.clone()); + + // todo: initialize the event registry properly on the root + + let edits = self.internal_dom.rebuild()?; + log::debug!("Received edits: {:#?}", edits); + edits.iter().for_each(|edit| { + log::debug!("patching with {:?}", edit); + patch_machine.handle_edit(edit); + }); + + patch_machine.reset(); + let root_node = body_element.first_child().unwrap(); + patch_machine.stack.push(root_node.clone()); + + // log::debug!("patch stack size {:?}", patch_machine.stack); + + // Event loop waits for the receiver to finish up + // TODO! Connect the sender to the virtual dom's suspense system + // Suspense is basically an external event that can force renders to specific nodes + while let Ok(event) = receiver.recv().await { + log::debug!("Stack before entrance {:#?}", patch_machine.stack.top()); + // log::debug!("patch stack size before {:#?}", patch_machine.stack); + // patch_machine.reset(); + // patch_machine.stack.push(root_node.clone()); + let edits = self.internal_dom.progress_with_event(event)?; + log::debug!("Received edits: {:#?}", edits); + + for edit in &edits { + log::debug!("edit stream {:?}", edit); + // log::debug!("Stream stack {:#?}", patch_machine.stack.top()); + patch_machine.handle_edit(edit); + } + + // log::debug!("patch stack size after {:#?}", patch_machine.stack); + patch_machine.reset(); + // our root node reference gets invalidated + // not sure why + // for now, just select the first child again. + // eventually, we'll just make our own root element instead of using body + // or just use body directly IDEK + let root_node = body_element.first_child().unwrap(); + patch_machine.stack.push(root_node.clone()); + } + + Ok(()) // should actually never return from this, should be an error, rustc just cant see it + } +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 4c4c53a2..b2e09007 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -29,7 +29,7 @@ async-channel = "1.6.1" # futures-lite = "1.11.3" [dependencies.web-sys] -version = "0.3" +version = "0.3.50" features = [ "Comment", "Document", @@ -71,7 +71,7 @@ crate-type = ["cdylib", "rlib"] [dev-dependencies] -uuid = { version = "0.8.2", features = ["v4"] } +uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } [[example]] name = "todomvc" diff --git a/packages/web/examples/deep.rs b/packages/web/examples/deep.rs index 2ee024e8..d3d16e6b 100644 --- a/packages/web/examples/deep.rs +++ b/packages/web/examples/deep.rs @@ -7,8 +7,6 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(CustomA)) } -use components::CustomB; - fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree { let (val, set_val) = use_state(&ctx, || "abcdef".to_string()); @@ -24,7 +22,7 @@ fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree { "Lower" onclick: move |_| set_val(val.to_ascii_lowercase()) } - CustomB { + components::CustomB { val: val.as_ref() } } diff --git a/packages/web/examples/demo2.rs b/packages/web/examples/demo2.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/packages/web/examples/demo2.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/web/examples/demoday.rs b/packages/web/examples/demoday.rs new file mode 100644 index 00000000..6e8923ec --- /dev/null +++ b/packages/web/examples/demoday.rs @@ -0,0 +1,86 @@ +use dioxus_web::{dioxus::prelude::*, WebsysRenderer}; + +fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)) +} + +fn App(ctx: Context, props: &()) -> DomTree { + ctx.render(rsx! { + main { class: "dark:bg-gray-800 bg-white relative h-screen" + NavBar {} + {(0..10).map(|f| rsx!{ Landing {} })} + } + }) +} + +fn NavBar(ctx: Context, props: &()) -> DomTree { + ctx.render(rsx!{ + header { class: "h-24 sm:h-32 flex items-center z-30 w-full" + div { class: "container mx-auto px-6 flex items-center justify-between" + div { class: "uppercase text-gray-800 dark:text-white font-black text-3xl" + svg { focusable:"false" width:"100" height:"100" viewBox: "0 0 512 309" + path { fill: "#000" + d: "M120.81 80.561h96.568v7.676h-87.716v57.767h82.486v7.675h-82.486v63.423h88.722v7.675H120.81V80.561zm105.22 0h10.26l45.467 63.423L328.23 80.56L391.441 0l-103.85 150.65l53.515 74.127h-10.663l-48.686-67.462l-48.888 67.462h-10.461l53.917-74.128l-50.296-70.088zm118.898 7.676V80.56h110.048v7.676h-50.699v136.54h-8.852V88.237h-50.497zM0 80.56h11.065l152.58 228.323l-63.053-84.107L9.254 91.468l-.402 133.31H0V80.56zm454.084 134.224c-1.809 0-3.165-1.4-3.165-3.212c0-1.81 1.356-3.212 3.165-3.212c1.83 0 3.165 1.401 3.165 3.212c0 1.811-1.335 3.212-3.165 3.212zm8.698-8.45h4.737c.064 2.565 1.937 4.29 4.693 4.29c3.079 0 4.823-1.854 4.823-5.325v-21.99h4.823v22.011c0 6.252-3.617 9.853-9.603 9.853c-5.62 0-9.473-3.493-9.473-8.84zm25.384-.28h4.78c.409 2.953 3.294 4.828 7.45 4.828c3.875 0 6.717-2.005 6.717-4.764c0-2.371-1.809-3.794-5.921-4.764l-4.005-.97c-5.62-1.316-8.181-4.032-8.181-8.602c0-5.54 4.521-9.227 11.303-9.227c6.308 0 10.916 3.686 11.196 8.925h-4.694c-.452-2.867-2.95-4.657-6.567-4.657c-3.81 0-6.35 1.833-6.35 4.635c0 2.22 1.635 3.493 5.683 4.441l3.423.841c6.373 1.488 9 4.075 9 8.753c0 5.95-4.607 9.68-11.97 9.68c-6.89 0-11.52-3.558-11.864-9.12z" + } + } + } + div { class:"flex items-center" + nav { class: "font-sen text-gray-800 dark:text-white uppercase text-lg lg:flex items-center hidden" + a { href: "#", class:"py-2 px-6 flex text-indigo-500 border-b-2 border-indigo-500" + "Home" + } + a { href: "#", class: "py-2 px-6 flex hover:text-indigo-500" + "Watch" + } + a { href: "#", class: "py-2 px-6 flex hover:text-indigo-500" + "Product" + } + a { href: "#", class: "py-2 px-6 flex hover:text-indigo-500" + "Contact" + } + a { href: "#", class: "py-2 px-6 flex hover:text-indigo-500" + "Career" + } + } + button { class: "lg:hidden flex flex-col ml-4" + span { class: "w-6 h-1 bg-gray-800 dark:bg-white mb-1" } + span { class: "w-6 h-1 bg-gray-800 dark:bg-white mb-1" } + span { class: "w-6 h-1 bg-gray-800 dark:bg-white mb-1" } + } + } + } + } + }) +} + +fn Landing(ctx: Context, props: &()) -> DomTree { + ctx.render(rsx!{ + div { class: "bg-white dark:bg-gray-800 flex relative z-20 items-center" + div { class: "container mx-auto px-6 flex flex-col justify-between items-center relative py-8" + div { class: "flex flex-col" + h1 { class: "font-light w-full uppercase text-center text-4xl sm:text-5xl dark:text-white text-gray-800" + "The React Framework for Production" + } + h2{ class: "font-light max-w-2xl mx-auto w-full text-xl dark:text-white text-gray-500 text-center py-8" + "Next.js gives you the best developer experience with all the features you need for production: \n + hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and \n + more. No config needed." + } + div { class: "flex items-center justify-center mt-4" + a { href: "#" class: "uppercase py-2 px-4 bg-gray-800 border-2 border-transparent text-white text-md mr-4 hover:bg-gray-900" + "Get started" + } + a{ href: "#" class: "uppercase py-2 px-4 bg-transparent border-2 border-gray-800 text-gray-800 dark:text-white hover:bg-gray-800 hover:text-white text-md" + "Documentation" + } + } + } + div { class: "block w-full mx-auto mt-6 md:mt-0 relative" + img { src: "/images/object/12.svg" class: "max-w-xs md:max-w-2xl m-auto" } + } + } + } + }) +} diff --git a/packages/web/examples/landingpage.rs b/packages/web/examples/landingpage.rs new file mode 100644 index 00000000..b834382c --- /dev/null +++ b/packages/web/examples/landingpage.rs @@ -0,0 +1,43 @@ +//! Basic example that renders a simple domtree to the browser. + +use std::rc::Rc; + +use dioxus_core::prelude::*; +use dioxus_web::*; +fn main() { + // Setup logging + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + // Run the app + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +static App: FC<()> = |ctx, _| { + let (contents, set_contents) = use_state(&ctx, || "asd"); + + 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 cloud app" } + div { class: "text-sm text-gray-500", "This is running in the cloud!!" } + 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" + } + } + } + } + } + }) +}; diff --git a/packages/web/examples/landingpage2.rs b/packages/web/examples/landingpage2.rs new file mode 100644 index 00000000..2d49f2df --- /dev/null +++ b/packages/web/examples/landingpage2.rs @@ -0,0 +1,43 @@ +//! Basic example that renders a simple domtree to the browser. + +use std::rc::Rc; + +use dioxus_core::prelude::*; +use dioxus_web::*; +fn main() { + // Setup logging + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + // Run the app + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +static App: FC<()> = |ctx, _| { + let (contents, set_contents) = use_state(&ctx, || "asd"); + + 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 Web app" } + div { class: "text-sm text-gray-500", "This is running in your browser!" } + 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" + } + } + } + } + } + }) +}; diff --git a/packages/web/examples/list.rs b/packages/web/examples/list.rs index 92808b49..422ef24d 100644 --- a/packages/web/examples/list.rs +++ b/packages/web/examples/list.rs @@ -1,125 +1,185 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; - -use dioxus::{events::on::MouseEvent, prelude::*}; use dioxus_core as dioxus; +use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; +use std::collections::BTreeMap; fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); console_error_panic_hook::set_once(); - wasm_bindgen_futures::spawn_local(async move { - WebsysRenderer::new_with_props(App, ()) - .run() - .await - .expect("major crash"); - }); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -use lazy_static::lazy_static; -lazy_static! { - static ref DummyData: BTreeMap = { - let vals = vec![ - "abc123", // - "abc124", // - "abc125", // - "abc126", // - "abc127", // - "abc128", // - "abc129", // - "abc1210", // - "abc1211", // - "abc1212", // - "abc1213", // - "abc1214", // - "abc1215", // - "abc1216", // - "abc1217", // - "abc1218", // - "abc1219", // - "abc1220", // - "abc1221", // - "abc1222", // - ]; - vals.into_iter() - .map(ToString::to_string) - .enumerate() - .collect() - }; +static APP_STYLE: &'static str = include_str!("./todomvc/style.css"); + +#[derive(PartialEq, Clone, Copy)] +pub enum FilterState { + All, + Active, + Completed, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct TodoItem { + pub id: uuid::Uuid, + pub checked: bool, + pub contents: String, } static App: FC<()> = |ctx, _| { - let items = use_state_new(&ctx, || DummyData.clone()); + let (draft, set_draft) = use_state(&ctx, || "".to_string()); + let (filter, set_filter) = use_state(&ctx, || FilterState::All); + let (is_editing, set_is_editing) = use_state(&ctx, || false); + // let (todos, set_todos) = use_state(&ctx, || BTreeMap::::new()); - // handle new elements - let add_new = move |_| { - items.modify(|m| { - let k = m.len(); - let v = match (k % 3, k % 5) { - (0, 0) => "FizzBuzz".to_string(), - (0, _) => "Fizz".to_string(), - (_, 0) => "Buzz".to_string(), - _ => k.to_string(), - }; - m.insert(k, v); - }) - }; - - let elements = items.iter().map(|(k, v)| { - rsx! { - ListHelper { - name: k, - value: v - onclick: move |_| { - let key = k.clone(); - items.modify(move |m| { m.remove(&key); } ) - } - } - } - }); + // let todos = use_ref(&ctx, || BTreeMap::::new()); + let todos = use_state_new(&ctx, || BTreeMap::::new()); + // let blah = "{draft}" ctx.render(rsx!( div { - h1 {"Some list"} - button { - "Remove all" - onclick: move |_| items.set(BTreeMap::new()) + id: "app" + style { "{APP_STYLE}" } + + div { + header { + class: "header" + h1 {"todos"} + button { + "press me" + onclick: move |evt| { + let contents = draft.clone(); + todos.modify(|f| { + let id = uuid::Uuid::new_v4(); + f.insert(id.clone(), TodoItem { + id, + checked: false, + contents + }); + }) + } + } + input { + class: "new-todo" + placeholder: "What needs to be done?" + // value: "{draft}" + oninput: move |evt| set_draft(evt.value) + } + } + + { // list + todos + .iter() + .filter(|(id, item)| match filter { + FilterState::All => true, + FilterState::Active => !item.checked, + FilterState::Completed => item.checked, + }) + .map(|(id, todo)| { + rsx!{ + li { + "{todo.contents}" + input { + class: "toggle" + type: "checkbox" + "{todo.checked}" + } + // {is_editing.then(|| rsx!( + // input { + // value: "{contents}" + // } + // ))} + } + } + }) + } + + // filter toggle (show only if the list isn't empty) + {(!todos.is_empty()).then(|| + rsx!{ + footer { + span { + strong {"10"} + span {"0 items left"} + } + ul { + class: "filters" + {[ + ("All", "", FilterState::All), + ("Active", "active", FilterState::Active), + ("Completed", "completed", FilterState::Completed), + ] + .iter() + .map(|(name, path, filter)| { + rsx!( + li { + class: "{name}" + a { + href: "{path}" + onclick: move |_| set_filter(filter.clone()) + "{name}" + } + } + ) + }) + }} + } + } + )} } - button { - "add new" - onclick: {add_new} - } - ul { - {elements} + + + footer { + class: "info" + p {"Double-click to edit a todo"} + p { + "Created by " + a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" } + } + p { + "Part of " + a { "TodoMVC", href: "http://todomvc.com" } + } } } )) }; -#[derive(Props)] -struct ListProps<'a, F: Fn(MouseEvent) + 'a> { - name: &'a usize, - value: &'a str, - onclick: F, -} +pub fn FilterToggles(ctx: Context, props: &()) -> DomTree { + // let reducer = recoil::use_callback(&ctx, || ()); + // let items_left = recoil::use_atom_family(&ctx, &TODOS, uuid::Uuid::new_v4()); -impl PartialEq for ListProps<'_, F> { - fn eq(&self, other: &Self) -> bool { - // no references are ever the same - false - } -} + let toggles = [ + ("All", "", FilterState::All), + ("Active", "active", FilterState::Active), + ("Completed", "completed", FilterState::Completed), + ] + .iter() + .map(|(name, path, _filter)| { + rsx!( + li { + class: "{name}" + a { + href: "{path}" + // onclick: move |_| reducer.set_filter(&filter) + "{name}" + } + } + ) + }); + + // todo + let item_text = ""; + let items_left = ""; -fn ListHelper(ctx: Context, props: &ListProps) -> DomTree { - let k = props.name; - let v = props.value; ctx.render(rsx! { - li { - class: "flex items-center text-xl" - key: "{k}" - span { "{k}: {v}" } - button { - "__ Remove" - onclick: {&props.onclick} + footer { + span { + strong {"{items_left}"} + span {"{item_text} left"} + } + ul { + class: "filters" + {toggles} } } }) diff --git a/packages/web/examples/todomvc/filtertoggles.rs b/packages/web/examples/todomvc/filtertoggles.rs index d3ade664..9a978692 100644 --- a/packages/web/examples/todomvc/filtertoggles.rs +++ b/packages/web/examples/todomvc/filtertoggles.rs @@ -13,16 +13,14 @@ pub fn FilterToggles(ctx: Context, props: &()) -> DomTree { ] .iter() .map(|(name, path, filter)| { - rsx!( - li { - class: "{name}" - a { - href: "{path}" - onclick: move |_| reducer.set_filter(&filter) - "{name}" - } + rsx!(li { + class: "{name}" + a { + "{name}" + href: "{path}" + onclick: move |_| reducer.set_filter(&filter) } - ) + }) }); // todo diff --git a/packages/web/examples/todomvc/todoitem.rs b/packages/web/examples/todomvc/todoitem.rs index e1fc26d6..e46c6c4d 100644 --- a/packages/web/examples/todomvc/todoitem.rs +++ b/packages/web/examples/todomvc/todoitem.rs @@ -21,7 +21,7 @@ pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { } {is_editing.then(|| rsx!( input { - value: "{contents}" + value: "{todo.contents}" } ))} } diff --git a/packages/web/examples/todomvc_simple.rs b/packages/web/examples/todomvc_simple.rs index b8077bb8..cfb9eb2c 100644 --- a/packages/web/examples/todomvc_simple.rs +++ b/packages/web/examples/todomvc_simple.rs @@ -101,13 +101,6 @@ pub struct TodoEntryProps { item: Rc, } -mod mac { - #[macro_export] - macro_rules! TodoEntry { - () => {}; - } -} - // pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree { // #[inline_props] pub fn TodoEntry( diff --git a/packages/web/examples/todomvcsingle.rs b/packages/web/examples/todomvcsingle.rs index 733e62db..1e39d889 100644 --- a/packages/web/examples/todomvcsingle.rs +++ b/packages/web/examples/todomvcsingle.rs @@ -115,6 +115,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_atom_family(&ctx, &TODOS, props.id); + let contents = ""; ctx.render(rsx! ( li { diff --git a/packages/web/examples/weather.rs b/packages/web/examples/weather.rs index 66d89a56..9855fa4a 100644 --- a/packages/web/examples/weather.rs +++ b/packages/web/examples/weather.rs @@ -17,7 +17,7 @@ fn main() {

// Title
- "Jon's awesome site!!11" + "Jon's awesome site!!"
// Subtext / description @@ -45,4 +45,3 @@ fn main() { }) })); } - diff --git a/packages/web/src/interpreter.rs b/packages/web/src/interpreter.rs index ae71b38d..bb7d28e5 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/interpreter.rs @@ -542,8 +542,14 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { "change" | "input" | "invalid" | "reset" | "submit" => { // is a special react events - // let evt: web_sys::FormEvent = event.clone().dyn_into().unwrap(); - todo!() + let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type"); + let value: Option = (&evt).data(); + let value = value.unwrap_or_default(); + // let value = (&evt).data().expect("No data to unwrap"); + + // todo - this needs to be a "controlled" event + // these events won't carry the right data with them + VirtualEvent::FormEvent(FormEvent { value }) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 5e74f249..ad16b329 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -5,11 +5,11 @@ use dioxus::prelude::Properties; use fxhash::FxHashMap; use web_sys::{window, Document, Element, Event, Node}; +// use futures::{channel::mpsc, SinkExt, StreamExt}; use dioxus::virtual_dom::VirtualDom; pub use dioxus_core as dioxus; use dioxus_core::{events::EventTrigger, prelude::FC}; -// use futures::{channel::mpsc, SinkExt, StreamExt}; pub use dioxus_core::prelude; pub mod interpreter; diff --git a/packages/webview/Cargo.toml b/packages/webview/Cargo.toml index d428bb9e..be19ad8e 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" } +dioxus-core = { path = "../core", version = "0.1.2", features = ["serde"] } 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 36e25758..f29a3095 100644 --- a/packages/webview/examples/demo.rs +++ b/packages/webview/examples/demo.rs @@ -4,50 +4,43 @@ use dioxus_core::prelude::*; fn main() { dioxus_webview::launch( - // Customize the webview |builder| { builder .title("Test Dioxus App") .size(320, 480) - .resizable(true) + .resizable(false) .debug(true) }, - // Props (), - // Draw the root component Example, ) .expect("Webview finished"); } static Example: FC<()> = |ctx, _props| { - ctx.render(html! { -
-
-
-
- // Title -
"Jon's awesome site!!11"
- - // Subtext / description -
"He worked so hard on it :)"
- -
- // Main number -
- "1337" -
-
- - // Try another - -
-
-
-
+ 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" + } + } + } + } + } }) };