Feat: major overhaul to diffing

This commit is contained in:
Jonathan Kelley 2021-05-15 12:03:08 -04:00
parent c809095124
commit 9810feebf5
37 changed files with 1470 additions and 1129 deletions

View File

@ -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",

View File

@ -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<dyn>)
- [ ] 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
- [ ] 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?)

View File

@ -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"

View File

@ -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<Ident>,
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<Ident>,
root: AmbiguousElement,
}
impl Parse for RsxRender {
fn parse(input: ParseStream) -> Result<Self> {
let fork = input.fork();
let root = { input.parse::<AmbiguousElement>() }?;
if !input.is_empty() {
return Err(Error::new(
input.span(),
"Currently only one element is allowed per component",
));
}
let custom_context = fork
.parse::<Ident>()
.and_then(|ident| {
fork.parse::<Token![,]>().map(|_| {
input.advance_to(&fork);
ident
})
})
.ok();
let forked = input.fork();
let name = forked.parse::<Ident>()?;
let root = match crate::util::is_valid_tag(&name.to_string()) {
true => input.parse::<Element>().map(|el| RootOption::Element(el)),
false => input.parse::<Component>().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! {
// 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
// }))
// },
let inner = &self.root;
let output = quote! {
dioxus::prelude::LazyNodes::new(move |ctx|{
let bump = ctx.bump;
#new_toks
#inner
})
},
};
output.to_tokens(out_tokens)
}
}
final_tokens.to_tokens(out_tokens);
enum AmbiguousElement {
Element(Element),
Component(Component),
}
impl Parse for AmbiguousElement {
fn parse(input: ParseStream) -> Result<Self> {
// Try to parse as an absolute path and immediately defer to the componetn
if input.peek(Token![::]) {
return input
.parse::<Component>()
.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::<syn::Path>() {
if pat.segments.len() > 1 {
return input
.parse::<Component>()
.map(|c| AmbiguousElement::Component(c));
}
}
if let Ok(name) = input.fork().parse::<Ident>() {
let name_str = name.to_string();
match is_valid_html_tag(&name_str) {
true => input
.parse::<Element>()
.map(|c| AmbiguousElement::Element(c)),
false => {
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input
.parse::<Component>()
.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::<TextNode>() {
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::<Expr>()?));
}
let fork = stream.fork();
if let Ok(element) = fork.parse::<Element>() {
stream.advance_to(&fork);
return Ok(Self::Element(element));
if stream.peek(LitStr) {
return Ok(Node::Text(stream.parse::<TextNode>()?));
}
let fork = stream.fork();
if let Ok(comp) = fork.parse::<Component>() {
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::<AmbiguousElement>()?))
}
}
@ -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::<ComponentField>() {
body.push(field);
}
body.push(content.parse::<ComponentField>()?);
// 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<Attr>,
attrs: Vec<ElementAttr>,
children: Vec<Node>,
}
impl Parse for Element {
fn parse(stream: ParseStream) -> Result<Self> {
//
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<ElementAttr> = vec![];
let mut children: Vec<Node> = 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::<Token![:]>().is_ok()
&& forked.parse::<Expr>().is_ok()
{
attrs.push(content.parse::<ElementAttr>()?);
} else {
children.push(content.parse::<Node>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas after elements/text
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
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<Self> {
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<Attr> = vec![];
let mut children: Vec<Node> = 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<Attr>,
children: &mut Vec<Node>,
) -> 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::<Token![,]>();
}
// [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::<Attr>() {
// 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::<Node>() {
// 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<Expr> {
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<Self> {
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::<LitStr>() {
s.advance_to(&fork);
AttrType::Value(rawtext)
} else {
s.parse()?
};
AttrType::Value(lit_str)
let toks = s.parse::<Expr>()?;
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::<Token![,]>();
}
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,19 +451,19 @@ 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),
}
// =======================================
@ -483,3 +491,9 @@ impl ToTokens for TextNode {
});
}
}
fn try_parse_bracketed(stream: &ParseBuffer) -> Result<Expr> {
let content;
syn::braced!(content in stream);
content.parse()
}

View File

@ -117,6 +117,9 @@ static VALID_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
"var",
"video",
"wbr",
// SVTG
"svg",
"path",
]
.iter()
.cloned()
@ -132,6 +135,6 @@ static VALID_TAGS: Lazy<HashSet<&'static str>> = 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)
}

View File

@ -34,4 +34,4 @@ log = "0.4.14"
serde = { version = "1.0.123", features = ["derive"], optional = true }
[features]
default = []
default = ["serde"]

View File

@ -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<T>
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<dyn Fn(i32)>,
}
impl PartialEq for ChildProps<'_> {

View File

@ -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<T> {
_val: std::marker::PhantomData<T>,
}
impl<T> Clone for ContextGuard2<T> {
// we aren't cloning the underlying data so clone isn't necessary
fn clone(&self) -> Self {
todo!()
}
}
impl<T> Copy for ContextGuard2<T> {}
impl<T> ContextGuard2<T> {
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<O> {
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() {}

View File

@ -1,63 +0,0 @@
// #[macro_use]
// use dioxus_core::ifmt;
// use fstrings::format_args_f;
fn main() {
let bump = bumpalo::Bump::new();
let _b = &bump;
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 = &bump;
// 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
// };
// }

View File

@ -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!()
}

View File

@ -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::<InternalHookState>().unwrap();
let internal_state = v
.downcast_mut::<InternalHookState>()
.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

View File

@ -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<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
PropsChanged {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
SameProps {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
stable_scope_addr: Weak<VCompAssociatedScope>,
root_id: u32,
},
Replace {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
old_scope: Weak<VCompAssociatedScope>,
new_scope: Weak<VCompAssociatedScope>,
id: u32,
root_id: u32,
},
Remove {
stable_scope_addr: Weak<VCompAssociatedScope>,
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 => {

View File

@ -21,7 +21,7 @@ mod use_state_def {
struct UseState<T: 'static> {
new_val: Rc<RefCell<Option<T>>>,
current_val: T,
caller: Box<dyn Fn(T) + 'static>,
caller: Rc<dyn Fn(T) + 'static>,
}
/// 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<dyn Fn(T)>) {
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);

View File

@ -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;
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<P> = 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<P> = for<'scope, 'r> fn(Context<'scope>, &'scope P) -> DomTree;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>;
// pub type FC<P> = 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.

View File

@ -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<Edit<'src>>;
// pub struct EditList<'src> {
// edits: Vec<Edit<'src>>,
// }
/// 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
@ -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

View File

@ -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<ScopeIdx>,
// our own index
pub myidx: ScopeIdx,
//
pub children: HashSet<ScopeIdx>,
pub caller: Weak<OpaqueComponent<'static>>,
// ==========================
// 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<Vec<Hook>>,
pub hook_arena: Vec<Hook>,
// 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<Vec<*const dyn Fn(VirtualEvent)>>,
}
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<OpaqueComponent<'creator_node>>,
myidx: ScopeIdx,
parent: Option<ScopeIdx>,
) -> 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<OpaqueComponent<'static>> = 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<OpaqueComponent<'creator_node>>) {
let broken_caller: Weak<OpaqueComponent<'static>> = 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<OpaqueComponent<'c>>;
let caller = std::mem::transmute::<ComComp<'static>, 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<usize>,
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! {
// <div onclick={|_| println!("Hell owlrld")}>
// "hello"
// </div>
// })
// };
#[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<dyn Fn(&Box<dyn Any>) -> bool>,
// used to actually run the component
// encapsulates props
runner: Box<dyn Fn(Context) -> DomTree>,
props_type: TypeId,
// the actual FC<T>
raw: *const (),
}
impl ComponentCaller {
fn new<P>(props: P) -> Self {
let comparator = Box::new(|f| false);
todo!();
// Self { comparator }
}
fn update_props<P>(props: P) {}
}
}

View File

@ -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<Scope>,
///
/// 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<Arena<Scope>>,
/// 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<OpaqueComponent<'static>>,
root_caller: Rc<OpaqueComponent<'static>>,
// 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<P: Properties + 'static>(root: FC<P>, 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<OpaqueComponent> = Rc::new(move |ctx| root(ctx, &root_props));
// the root is kept around with a "hard" allocation
let root_caller: Rc<OpaqueComponent> = 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<OpaqueComponent> = 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::<P>(),
}
}
// 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<EditList<'s>> {
// Diff from the top
let mut diff_machine = DiffMachine::new();
let very_unsafe_components = &mut self.components as *mut generational_arena::Arena<Scope>;
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());
todo!()
}
}
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;
// }
#[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)
}
}
let edits: Vec<Edit<'s>> = diff_machine.consume();
Ok(edits)
impl PartialOrd for HeightMarker {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<EditList<'_>> {
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<EditList> {
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::<ScopeIdx>::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")
}
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HierarchyMarker {
source: ScopeIdx,
Ok(diff_machine.consume())
}
}
#[derive(Debug, Default, PartialEq, Clone)]
pub struct UpdateFunnel(Rc<RefCell<Vec<HierarchyMarker>>>);
impl Drop for VirtualDom {
fn drop(&mut self) {
// Drop all the components first
// self.components.drain();
// Finally, drop the root caller
unsafe {
// let root: Box<OpaqueComponent<'static>> =
// Box::from_raw(self.root_caller as *const OpaqueComponent<'static> as *mut _);
// std::mem::drop(root);
}
}
}
#[derive(Debug, Default, Clone)]
pub struct UpdateFunnel(Rc<RefCell<Vec<HeightMarker>>>);
impl UpdateFunnel {
fn schedule_update(&self, source: ScopeIdx) -> impl Fn() {
fn schedule_update(&self, source: &Scope) -> impl Fn() {
let inner = self.clone();
move || {
inner
.0
.as_ref()
.borrow_mut()
.push(HierarchyMarker { source })
}
}
}
macro_rules! UpdateFunnel {
(root: $root:expr) => {
VirtualDom::new($root)
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<ScopeIdx>,
// 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<ScopeIdx>,
// caller: &'static OpaqueComponent<'static>,
pub caller: Weak<OpaqueComponent<'static>>,
// ==========================
// 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<Vec<Hook>>,
// 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<Vec<*const dyn Fn(VirtualEvent)>>,
}
// #[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<OpaqueComponent<'creator_node>>,
myidx: ScopeIdx,
parent: Option<ScopeIdx>,
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<OpaqueComponent<'static>> = 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<OpaqueComponent<'creator_node>>) {
let broken_caller: Weak<OpaqueComponent<'static>> = 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<OpaqueComponent<'c>>;
let caller = std::mem::transmute::<ComComp<'static>, 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<usize>,
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<T: 'static> {
// resource: T,
// scope: ScopeIdx,
// gen: u32,
// }
}

View File

@ -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<T, G>(ctx: &Context<G>, 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<T> 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<K, V> Collection for std::collections::HashMap<K, V> {}
struct MapCollection<K, V> {
inner: HashMap<K, V>,
}
impl<K, V> MapCollection<K, V> {
fn set(&self, key: K, val: V) {
//
}
}

12
packages/ios/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "dioxus-ios"
version = "0.0.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
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"

3
packages/ios/README.md Normal file
View File

@ -0,0 +1,3 @@
# Dioxus for iOS
This crate implements a renderer of the Dioxus DomTree to an iOS app

70
packages/ios/src/lib.rs Normal file
View File

@ -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::<EventTrigger>();
// 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
}
}

View File

@ -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"

View File

@ -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()
}
}

View File

@ -0,0 +1 @@
fn main() {}

View File

@ -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 &amp; 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" }
}
}
}
})
}

View File

@ -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"
}
}
}
}
}
})
};

View File

@ -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"
}
}
}
}
}
})
};

View File

@ -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<usize, String> = {
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::<String, TodoItem>::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::<String, TodoItem>::new());
let todos = use_state_new(&ctx, || BTreeMap::<uuid::Uuid, TodoItem>::new());
// let blah = "{draft}"
ctx.render(rsx!(
div {
h1 {"Some list"}
id: "app"
style { "{APP_STYLE}" }
div {
header {
class: "header"
h1 {"todos"}
button {
"Remove all"
onclick: move |_| items.set(BTreeMap::new())
"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
});
})
}
button {
"add new"
onclick: {add_new}
}
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 {
{elements}
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}"
}
}
)
})
}}
}
}
)}
}
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<F: Fn(MouseEvent)> PartialEq for ListProps<'_, F> {
fn eq(&self, other: &Self) -> bool {
// no references are ever the same
false
}
}
fn ListHelper<F: Fn(MouseEvent)>(ctx: Context, props: &ListProps<F>) -> DomTree {
let k = props.name;
let v = props.value;
ctx.render(rsx! {
let toggles = [
("All", "", FilterState::All),
("Active", "active", FilterState::Active),
("Completed", "completed", FilterState::Completed),
]
.iter()
.map(|(name, path, _filter)| {
rsx!(
li {
class: "flex items-center text-xl"
key: "{k}"
span { "{k}: {v}" }
button {
"__ Remove"
onclick: {&props.onclick}
class: "{name}"
a {
href: "{path}"
// onclick: move |_| reducer.set_filter(&filter)
"{name}"
}
}
)
});
// todo
let item_text = "";
let items_left = "";
ctx.render(rsx! {
footer {
span {
strong {"{items_left}"}
span {"{item_text} left"}
}
ul {
class: "filters"
{toggles}
}
}
})

View File

@ -13,16 +13,14 @@ pub fn FilterToggles(ctx: Context, props: &()) -> DomTree {
]
.iter()
.map(|(name, path, filter)| {
rsx!(
li {
rsx!(li {
class: "{name}"
a {
"{name}"
href: "{path}"
onclick: move |_| reducer.set_filter(&filter)
"{name}"
}
}
)
})
});
// todo

View File

@ -21,7 +21,7 @@ pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree {
}
{is_editing.then(|| rsx!(
input {
value: "{contents}"
value: "{todo.contents}"
}
))}
}

View File

@ -101,13 +101,6 @@ pub struct TodoEntryProps {
item: Rc<TodoItem>,
}
mod mac {
#[macro_export]
macro_rules! TodoEntry {
() => {};
}
}
// pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> DomTree {
// #[inline_props]
pub fn TodoEntry(

View File

@ -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 {

View File

@ -17,7 +17,7 @@ fn main() {
<div class="flex flex-col bg-white rounded p-4 w-full max-w-xs">
// Title
<div class="font-bold text-xl">
"Jon's awesome site!!11"
"Jon's awesome site!!"
</div>
// Subtext / description
@ -45,4 +45,3 @@ fn main() {
})
}));
}

View File

@ -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<String> = (&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"

View File

@ -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;

View File

@ -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"

View File

@ -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! {
<div>
<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">
// Title
<div class="font-bold text-xl"> "Jon's awesome site!!11" </div>
// Subtext / description
<div class="text-sm text-gray-500"> "He worked so hard on it :)" </div>
<div class="flex flex-row items-center justify-center mt-6">
// Main number
<div class="font-medium text-6xl">
"1337"
</div>
</div>
// Try another
<div class="flex flex-row justify-between mt-6">
<a href="http://localhost:8080/fib/{}" class="underline">
"Legit made my own React"
</a>
</div>
</div>
</div>
</div>
</div>
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"
}
}
}
}
}
})
};