diff --git a/Cargo.toml b/Cargo.toml index 160efff13..917e331de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ members = [ "packages/html-namespace", "packages/web", # "packages/webview" - # "packages/cli", + "packages/cli", # "packages/atoms", # "packages/ssr", # "packages/docsite", diff --git a/examples/assets/calculator.css b/examples/assets/calculator.css new file mode 100644 index 000000000..808f5e56c --- /dev/null +++ b/examples/assets/calculator.css @@ -0,0 +1,136 @@ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + +body { + margin: 0; + font: 100 14px 'Roboto'; +} + +button { + display: block; + background: none; + border: none; + padding: 0; + font-family: inherit; + user-select: none; + cursor: pointer; + outline: none; + + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +button:active { + box-shadow: inset 0px 0px 80px 0px rgba(0,0,0,0.25); +} + +#wrapper { + height: 100vh; + + display: flex; + align-items: center; + justify-content: center; +} + +#app { + width: 320px; + height: 520px; + position: relative; +} + +.calculator { + width: 100%; + height: 100%; + background: black; + + display: flex; + flex-direction: column; +} + +#wrapper .calculator { + box-shadow: 0px 0px 20px 0px #aaa; +} + +.calculator-display { + color: white; + background: #1c191c; + line-height: 130px; + font-size: 6em; + + flex: 1; +} + +.auto-scaling-text { + display: inline-block; +} + +.calculator-display .auto-scaling-text { + padding: 0 30px; + position: absolute; + right: 0; + transform-origin: right; +} + +.calculator-keypad { + height: 400px; + + display: flex; +} + +.calculator .input-keys { + width: 240px; +} + +.calculator .function-keys { + display: flex; +} + +.calculator .digit-keys { + background: #e0e0e7; + + display: flex; + flex-direction: row; + flex-wrap: wrap-reverse; +} + +.calculator-key { + width: 80px; + height: 80px; + border-top: 1px solid #777; + border-right: 1px solid #666; + text-align: center; + line-height: 80px; +} +.calculator .function-keys .calculator-key { + font-size: 2em; +} +.calculator .function-keys .key-multiply { + line-height: 50px; +} +.calculator .digit-keys .calculator-key { + font-size: 2.25em; +} +.calculator .digit-keys .key-0 { + width: 160px; + text-align: left; + padding-left: 32px; +} +.calculator .digit-keys .key-dot { + padding-top: 1em; + font-size: 0.75em; +} +.calculator .operator-keys .calculator-key { + color: white; + border-right: 0; + font-size: 3em; +} + +.calculator .function-keys { + background: linear-gradient(to bottom, rgba(202,202,204,1) 0%, rgba(196,194,204,1) 100%); +} +.calculator .operator-keys { + background: linear-gradient(to bottom, rgba(252,156,23,1) 0%, rgba(247,126,27,1) 100%); +} diff --git a/examples/calculator.rs b/examples/calculator.rs index db1c422dd..ded72feec 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -98,16 +98,16 @@ static App: FC<()> = |cx| { CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "●" } - {(1..9).map(|k| rsx!{ + {(1..9).map(move |k| rsx!{ CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } })} } div { class: "operator-keys" - CalculatorKey { name:"key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" } - CalculatorKey { name:"key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" } - CalculatorKey { name:"key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "−" } - CalculatorKey { name:"key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" } - CalculatorKey { name:"key-equals", onclick: move |_| perform_operation() "=" } + CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" } + CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" } + CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "−" } + CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" } + CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" } } } } @@ -116,7 +116,10 @@ static App: FC<()> = |cx| { #[derive(Props)] struct CalculatorKeyProps<'a> { + /// Name! name: &'static str, + + /// Click! onclick: &'a dyn Fn(MouseEvent), } diff --git a/examples/compose.rs b/examples/compose.rs new file mode 100644 index 000000000..df982e3d0 --- /dev/null +++ b/examples/compose.rs @@ -0,0 +1,60 @@ +fn main() {} + +struct Title(String); + +struct Position([f32; 3]); + +struct Velocity([f32; 3]); + +type Batch = fn(&mut T) -> (); + +static Atom: Batch<(Title, Position, Velocity)> = |_| {}; + +enum VNode<'a> { + El(El<'a>), + Text(&'a str), + Fragment(&'a [VNode<'a>]), +} +struct El<'a> { + name: &'static str, + key: Option<&'a str>, + attrs: &'a [(&'static str, AttrType<'a>)], + children: &'a [El<'a>], +} +enum AttrType<'a> { + Numeric(usize), + Text(&'a str), +} + +fn example() { + use AttrType::Numeric; + let el = El { + name: "div", + attrs: &[("type", Numeric(10)), ("type", Numeric(10))], + key: None, + children: &[], + }; +} + +use dioxus::prelude::bumpalo::Bump; +trait IntoVnode { + fn into_vnode<'a>(self, b: &'a Bump) -> VNode<'a>; +} + +impl<'a> IntoIterator for VNode<'a> { + type Item = VNode<'a>; + type IntoIter = std::iter::Once>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + std::iter::once(self) + } +} + +fn take_iterable(f: impl IntoIterator) { + let iter = f.into_iter(); + let b = Bump::new(); + for f in iter { + let v = f.into_vnode(&b); + } +} diff --git a/examples/doggo.rs b/examples/doggo.rs index e69de29bb..668062fd8 100644 --- a/examples/doggo.rs +++ b/examples/doggo.rs @@ -0,0 +1,16 @@ +use dioxus::prelude::*; +fn main() {} + +static Example: FC<()> = |cx| { + let (g, set_g) = use_state(&cx, || 0); + let v = (0..10).map(move |f| { + rsx!(li { + onclick: move |_| set_g(10) + }) + }); + cx.render(rsx! { + div { + {v} + } + }) +}; diff --git a/examples/model.rs b/examples/model.rs index 727b3a28a..712aa1cc3 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -4,51 +4,54 @@ //! Some components benefit through the use of "Models". Models are a single block of encapsulated state that allow mutative //! methods to be performed on them. Dioxus exposes the ability to use the model pattern through the "use_model" hook. //! +//! Models are commonly used in the "Model-View-Component" approach for building UI state. +//! //! `use_model` is basically just a fancy wrapper around set_state, but saves a "working copy" of the new state behind a -//! RefCell. To modify the working copy, you need to call "modify" which returns the RefMut. This makes it easy to write +//! RefCell. To modify the working copy, you need to call "get_mut" which returns the RefMut. This makes it easy to write //! fully encapsulated apps that retain a certain feel of native Rusty-ness. A calculator app is a good example of when this //! is useful. +//! +//! Do note that "get_mut" returns a `RefMut` (a lock over a RefCell). If two `RefMut`s are held at the same time (ie in a loop) +//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two +//! RefMuts at the same time. use dioxus::events::on::*; use dioxus::prelude::*; -use std::{ - cell::RefCell, - ops::{Deref, DerefMut}, -}; fn main() { dioxus::webview::launch(App) } static App: FC<()> = |cx| { - let state = use_model(&cx, || CalculatorState::new()); + let calc = use_model(&cx, || Calculator::new()); - let clear_display = state.display_value.eq("0"); + let clear_display = calc.display_value.eq("0"); let clear_text = if clear_display { "C" } else { "AC" }; - let formatted = state.formatted(); + let formatted = calc.formatted(); cx.render(rsx! { - div { class: "calculator", onkeydown: move |evt| state.modify().handle_keydown(evt), - div { class: "calculator-display", "{formatted}"} + div { class: "calculator", onkeydown: move |evt| calc.get_mut().handle_keydown(evt), + div { class: "calculator-display","{formatted}"} div { class: "input-keys" div { class: "function-keys" - CalculatorKey { name: "key-clear", onclick: move |_| state.modify().clear_display() "{clear_text}" } - CalculatorKey { name: "key-sign", onclick: move |_| state.modify().toggle_sign(), "±"} - CalculatorKey { name: "key-percent", onclick: move |_| state.modify().toggle_percent() "%"} + // All there ways that the calculator may be modified: + CalculatorKey { name: "key-clear", onclick: move |_| calc.get_mut().clear_display() "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: move |_| calc.modify(Calculator::toggle_sign), "±"} + CalculatorKey { name: "key-percent", onclick: move |_| calc.modify(|f| f.toggle_percent()) "%"} } div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| state.modify().input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| state.modify().input_dot(), "●" } - {(1..9).map(|k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.modify().input_digit(k), "{k}" } + CalculatorKey { name: "key-0", onclick: move |_| calc.get_mut().input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| calc.get_mut().input_dot(), "●" } + {(1..9).map(move |k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| calc.get_mut().input_digit(k), "{k}" } })} } div { class: "operator-keys" - CalculatorKey { name:"key-divide", onclick: move |_| state.modify().set_operator(Operator::Div) "÷" } - CalculatorKey { name:"key-multiply", onclick: move |_| state.modify().set_operator(Operator::Mul) "×" } - CalculatorKey { name:"key-subtract", onclick: move |_| state.modify().set_operator(Operator::Sub) "−" } - CalculatorKey { name:"key-add", onclick: move |_| state.modify().set_operator(Operator::Add) "+" } - CalculatorKey { name:"key-equals", onclick: move |_| state.modify().perform_operation() "=" } + CalculatorKey { name:"key-divide", onclick: move |_| calc.get_mut().set_operator(Operator::Div) "÷" } + CalculatorKey { name:"key-multiply", onclick: move |_| calc.get_mut().set_operator(Operator::Mul) "×" } + CalculatorKey { name:"key-subtract", onclick: move |_| calc.get_mut().set_operator(Operator::Sub) "−" } + CalculatorKey { name:"key-add", onclick: move |_| calc.get_mut().set_operator(Operator::Add) "+" } + CalculatorKey { name:"key-equals", onclick: move |_| calc.get_mut().perform_operation() "=" } } } } @@ -56,7 +59,7 @@ static App: FC<()> = |cx| { }; #[derive(Clone)] -struct CalculatorState { +struct Calculator { display_value: String, operator: Option, cur_val: f64, @@ -68,9 +71,10 @@ enum Operator { Mul, Div, } -impl CalculatorState { + +impl Calculator { fn new() -> Self { - CalculatorState { + Calculator { display_value: "0".to_string(), operator: None, cur_val: 0.0, @@ -118,7 +122,9 @@ impl CalculatorState { self.display_value.pop(); } } - fn set_operator(&mut self, operator: Operator) {} + fn set_operator(&mut self, operator: Operator) { + self.operator = Some(operator) + } fn handle_keydown(&mut self, evt: KeyboardEvent) { match evt.key_code() { KeyCode::Backspace => self.backspace(), @@ -160,6 +166,10 @@ fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { use odl::use_model; mod odl { use super::*; + use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + }; /// Use model makes it easy to use "models" as state for components. To modify the model, call "modify" and a clone of the /// current model will be made, with a RefMut lock on it. Dioxus will never run your components multithreaded, so you can /// be relatively sure that this won't fail in practice @@ -208,9 +218,14 @@ mod odl { wip: RefCell::new(t), } } - pub fn modify(&self) -> RefMut<'_, T> { + pub fn get_mut(&self) -> RefMut<'_, T> { self.wip.borrow_mut() } + pub fn modify(&self, f: impl FnOnce(&mut T)) { + let mut g = self.get_mut(); + let r = g.deref_mut(); + todo!() + } } #[derive(Clone, Copy)] diff --git a/examples/reference/iterators.rs b/examples/reference/iterators.rs index 096f0d9e0..d069d1099 100644 --- a/examples/reference/iterators.rs +++ b/examples/reference/iterators.rs @@ -2,9 +2,17 @@ use dioxus::prelude::*; fn main() {} static Example: FC<()> = |cx| { + let (g, set_g) = use_state(&cx, || 0); + let v = (0..10).map(|f| { + rsx! { + li { + onclick: move |_| set_g(10) + } + } + }); cx.render(rsx! { div { - + {v} } }) }; diff --git a/examples/reference/main.rs b/examples/reference/main.rs new file mode 100644 index 000000000..a995ffc02 --- /dev/null +++ b/examples/reference/main.rs @@ -0,0 +1,7 @@ +// mod iterator; +// mod memo; +mod iterators; +mod listener; +mod memo; + +fn main() {} diff --git a/examples/reference/mod.rs b/examples/reference/mod.rs deleted file mode 100644 index f08cae033..000000000 --- a/examples/reference/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod iterator; -mod memo; diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index bf3f85254..1d06b3ae8 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -70,15 +70,20 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> { // [3] Bindgen the final binary for use easy linking let mut bindgen_builder = Bindgen::new(); + let release_type = match config.release { + true => "release", + false => "debug", + }; + let input_path = match executable { ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir // .join("wasm32-unknown-unknown/release") - .join("wasm32-unknown-unknown/debug") + .join(format!("wasm32-unknown-unknown/{}", release_type)) .join(format!("{}.wasm", name)), ExecutableType::Example(name) => target_dir // .join("wasm32-unknown-unknown/release/examples") - .join("wasm32-unknown-unknown/debug/examples") + .join(format!("wasm32-unknown-unknown/{}/examples", release_type)) .join(format!("{}.wasm", name)), }; diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 3827bfb9b..d7180d3a9 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -68,19 +68,43 @@ impl ToTokens for Element { let name = &self.name.to_string(); tokens.append_all(quote! { - dioxus::builder::ElementBuilder::new(__cx, #name) + __cx.element(#name) }); + // dioxus::builder::ElementBuilder::new(__cx, #name) + // dioxus::builder::ElementBuilder::new(__cx, #name) + // Add attributes + // TODO: conver to the "attrs" syntax for compile-time known sizes for attr in self.attrs.iter() { attr.to_tokens(tokens); } - let mut children = self.children.iter(); - while let Some(child) = children.next() { - let inner_toks = child.to_token_stream(); + // let mut children = self.children.iter(); + // while let Some(child) = children.next() { + // let inner_toks = child.to_token_stream(); + // tokens.append_all(quote! { + // .iter_child(#inner_toks) + // }) + // } + + let mut childs = quote! {}; + for child in &self.children { + match child { + Node::Text(e) => e.to_tokens(&mut childs), + Node::Element(e) => e.to_tokens(&mut childs), + Node::RawExpr(e) => quote! { + __cx.fragment_from_iter(#e) + } + .to_tokens(&mut childs), + } + childs.append_all(quote! {,}) + } + if self.children.len() > 0 { tokens.append_all(quote! { - .iter_child(#inner_toks) - }) + .children([ + #childs + ]) + }); } tokens.append_all(quote! { diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index 7a4579bf7..a10c8d7cc 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -99,14 +99,14 @@ impl ToTokens for RsxRender { // The `in cx` pattern allows directly rendering Some(ident) => out_tokens.append_all(quote! { #ident.render(dioxus::prelude::LazyNodes::new(move |__cx|{ - let bump = &__cx.bump(); + // let bump = &__cx.bump(); #inner })) }), // Otherwise we just build the LazyNode wrapper None => out_tokens.append_all(quote! { dioxus::prelude::LazyNodes::new(move |__cx: &NodeFactory|{ - let bump = &__cx.bump(); + // let bump = &__cx.bump(); #inner }) }), diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs index 744012b21..dc91ca3a8 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/core-macro/src/rsx/node.rs @@ -61,13 +61,7 @@ impl ToTokens for TextNode { // todo: use heuristics to see if we can promote to &static str let token_stream = &self.0.to_token_stream(); tokens.append_all(quote! { - { - // use bumpalo::core_alloc::fmt::Write; - // let mut s = bumpalo::collections::String::new_in(bump); - // s.write_fmt(format_args_f!(#token_stream)).unwrap(); - dioxus::builder::text3(bump, format_args_f!(#token_stream)) - // dioxus::builder::text2(s) - } + __cx.text(format_args_f!(#token_stream)) }); } } diff --git a/packages/core-macro/src/util.rs b/packages/core-macro/src/util.rs index f38212943..84c6391d6 100644 --- a/packages/core-macro/src/util.rs +++ b/packages/core-macro/src/util.rs @@ -105,7 +105,7 @@ static HTML_TAGS: Lazy> = Lazy::new(|| { "source", "span", "strong", - // "style", + "style", "sub", "summary", "sup", diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e2674fe59..9b8574cb8 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -135,197 +135,357 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later. When re-entering, we reuse the EditList in DiffState */ - match old_node { - VNode::Element(old) => match new_node { - // New node is an element, old node was en element, need to investiage more deeply - VNode::Element(new) => { - // If the element type is completely different, the element needs to be re-rendered completely - // This is an optimization React makes due to how users structure their code - if new.tag_name != old.tag_name || new.namespace != old.namespace { - self.create(new_node); - self.dom.replace_with(); - return; - } - new.dom_id.set(old.dom_id.get()); - - self.diff_listeners(old.listeners, new.listeners); - self.diff_attr(old.attributes, new.attributes, new.namespace); - self.diff_children(old.children, new.children); - } - // New node is a text element, need to replace the element with a simple text node - VNode::Text(_) => { - self.create(new_node); - self.dom.replace_with(); - } - - // New node is a component - // Make the component and replace our element on the stack with it - VNode::Component(_) => { - self.create(new_node); - self.dom.replace_with(); - } - - // New node is actually a sequence of nodes. - // We need to replace this one node with a sequence of nodes - // Not yet implement because it's kinda hairy - VNode::Fragment(new) => { - match new.children.len() { - 0 => { - // remove - } - 1 => { - // replace - self.create(&new.children[0]); - self.dom.replace_with(); - } - _ => { - todo!() - // remove and mount many - // self.remove_self_and_next_siblings(old) - // - // let iter = ChildIterator::new(new_node, self.components).rev(); - // for child in iter {} - // for child in new.children.iter().skip(1).rev() { - // self.dom.remove(); - // } - } - } - } - - // New Node is actually suspended. Todo - VNode::Suspended { real } => todo!(), - }, - - // Old element was text - VNode::Text(old) => match new_node { - VNode::Text(new) => { - if old.text != new.text { - log::debug!("Text has changed {}, {}", old.text, new.text); - self.dom.set_text(new.text); - } - new.dom_id.set(old.dom_id.get()); - } - VNode::Element(_) | VNode::Component(_) => { - self.create(new_node); - self.dom.replace_with(); - } - - // TODO on handling these types - VNode::Fragment(frag) => { - if frag.children.len() == 0 { - // do nothing - } else { - self.create(&frag.children[0]); - self.dom.replace_with(); - for child in frag.children.iter().skip(1) { - self.create(child); - self.dom.append_child(); - } - } - } - VNode::Suspended { real } => todo!(), - }, - - // Old element was a component - VNode::Component(old) => { - match new_node { - // It's something entirely different - VNode::Element(_) | VNode::Text(_) => { - self.create(new_node); - self.dom.replace_with(); - } - - // It's also a component - VNode::Component(new) => { - if old.user_fc == new.user_fc { - // Make sure we're dealing with the same component (by function pointer) - - // Make sure the new component vnode is referencing the right scope id - let scope_id = old.ass_scope.get(); - new.ass_scope.set(scope_id); - new.mounted_root.set(old.mounted_root.get()); - - // make sure the component's caller function is up to date - self.components - .with_scope(scope_id.unwrap(), |scope| { - scope.caller = Rc::downgrade(&new.caller) - }) - .unwrap(); - - // React doesn't automatically memoize, but we do. - // The cost is low enough to make it worth checking - // Rust produces fairly performant comparison methods, sometimes SIMD accelerated - let should_render = match old.comparator { - Some(comparator) => comparator(new), - None => true, - }; - - if should_render { - // // self.dom.commit_traversal(); - self.components - .with_scope(scope_id.unwrap(), |f| { - f.run_scope().unwrap(); - }) - .unwrap(); - // diff_machine.change_list.load_known_root(root_id); - // run the scope - // - } else { - // Component has memoized itself and doesn't need to be re-rendered. - // We still need to make sure the child's props are up-to-date. - // Don't commit traversal - } - } else { - // It's an entirely different component - - // A new component has shown up! We need to destroy the old node - - // Wipe the old one and plant the new one - // self.dom.commit_traversal(); - // self.dom.replace_node_with(old.dom_id, new.dom_id); - // self.create(new_node); - // self.dom.replace_with(); - self.create(new_node); - // self.create_and_repalce(new_node, old.mounted_root.get()); - - // Now we need to remove the old scope and all of its descendents - let old_scope = old.ass_scope.get().unwrap(); - self.destroy_scopes(old_scope); - } - } - VNode::Fragment(_) => todo!(), - VNode::Suspended { real } => todo!(), + log::debug!("diffing..."); + match (old_node, new_node) { + // Handle the "sane" cases first. + // The rsx and html macros strongly discourage dynamic lists not encapsulated by a "Fragment". + // So the sane (and fast!) cases are where the virtual structure stays the same and is easily diffable. + (VNode::Text(old), VNode::Text(new)) => { + if old.text != new.text { + self.dom.push_root(old.dom_id.get()); + log::debug!("Text has changed {}, {}", old.text, new.text); + self.dom.set_text(new.text); } + new.dom_id.set(old.dom_id.get()); } - VNode::Fragment(old) => { + (VNode::Element(old), VNode::Element(new)) => { + // If the element type is completely different, the element needs to be re-rendered completely + // This is an optimization React makes due to how users structure their code // - match new_node { - VNode::Fragment(new) => todo!(), + // In Dioxus, this is less likely to occur unless through a fragment + if new.tag_name != old.tag_name || new.namespace != old.namespace { + log::debug!("things have changed....?"); + self.dom.push_root(old.dom_id.get()); + self.create(new_node); + self.dom.replace_with(); + return; + } + log::debug!("Important Things haven't changed!"); + new.dom_id.set(old.dom_id.get()); - // going from fragment to element means we're going from many (or potentially none) to one - VNode::Element(new) => {} - VNode::Text(_) => todo!(), - VNode::Suspended { real } => todo!(), - VNode::Component(_) => todo!(), + self.diff_listeners(old.listeners, new.listeners); + self.diff_attr(old.attributes, new.attributes, new.namespace); + self.diff_children(old.children, new.children); + } + + (VNode::Component(old), VNode::Component(new)) => { + log::warn!("diffing components? {:#?}", new.user_fc); + if old.user_fc == new.user_fc { + // Make sure we're dealing with the same component (by function pointer) + + // Make sure the new component vnode is referencing the right scope id + let scope_id = old.ass_scope.get(); + new.ass_scope.set(scope_id); + + // new.mounted_root.set(old.mounted_root.get()); + + // make sure the component's caller function is up to date + let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap(); + // .with_scope(scope_id.unwrap(), |scope| { + scope.caller = Rc::downgrade(&new.caller); + scope.update_children(new.children); + // }) + // .unwrap(); + + // React doesn't automatically memoize, but we do. + // The cost is low enough to make it worth checking + // Rust produces fairly performant comparison methods, sometimes SIMD accelerated + // let should_render = match old.comparator { + // Some(comparator) => comparator(new), + // None => true, + // }; + + // if should_render { + // // self.dom.commit_traversal(); + // let f = self.components.try_get_mut(scope_id.unwrap()).unwrap(); + // self.components + // .with_scope(scope_id.unwrap(), |f| { + log::debug!("running scope during diff {:#?}", scope_id); + scope.run_scope().unwrap(); + self.diff_node(scope.old_frame(), scope.next_frame()); + log::debug!("scope completed {:#?}", scope_id); + self.seen_nodes.insert(scope_id.unwrap()); + // }) + // .unwrap(); + + // diff_machine.change_list.load_known_root(root_id); + // run the scope + // + // } else { + // log::error!("Memoized componented"); + // // Component has memoized itself and doesn't need to be re-rendered. + // // We still need to make sure the child's props are up-to-date. + // // Don't commit traversal + // } + } else { + // It's an entirely different component + + // A new component has shown up! We need to destroy the old node + + // Wipe the old one and plant the new one + // self.dom.commit_traversal(); + // self.dom.replace_node_with(old.dom_id, new.dom_id); + // self.create(new_node); + log::warn!("creating and replacing..."); + self.create(new_node); + + // self.dom.replace_with(); + // self.create_and_repalce(new_node, old.mounted_root.get()); + + // Now we need to remove the old scope and all of its descendents + let old_scope = old.ass_scope.get().unwrap(); + self.destroy_scopes(old_scope); } } - // a suspended node will perform a mem-copy of the previous elements until it is ready - // this means that event listeners will need to be disabled and removed - // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated - VNode::Suspended { real: old_real } => { + (VNode::Fragment(old), VNode::Fragment(new)) => { + log::debug!("diffing as fragment"); + // This is the case where options or direct vnodes might be used. + // In this case, it's faster to just skip ahead to their diff + if old.children.len() == 1 && new.children.len() == 1 { + self.diff_node(old.children.get(0).unwrap(), new.children.get(0).unwrap()); + return; + } + + // Diff using the approach where we're looking for added or removed nodes. + if old.children.len() != new.children.len() {} + + // Diff where we think the elements are the same + if old.children.len() == new.children.len() {} + + self.diff_children(old.children, new.children); + // todo!() + } + + // Okay - these are the "insane" cases where the structure is entirely different. + // The factory and rsx! APIs don't really produce structures like this, so we don't take any too complicated + // code paths. + + // in the case where the old node was a fragment but the new nodes are text, + (VNode::Fragment(_) | VNode::Component(_), VNode::Element(_) | VNode::Text(_)) => { + // find the first real element int the old node + let mut iter = RealChildIterator::new(old_node, self.components); + if let Some(first) = iter.next() { + // replace the very first node with the creation of the element or text + } else { + // there are no real elements in the old fragment... + // We need to load up the next real + } + if let VNode::Component(old) = old_node { + // schedule this component's destructor to be run + todo!() + } + } + // In the case where real nodes are being replaced by potentially + (VNode::Element(_) | VNode::Text(_), VNode::Fragment(new)) => { // - match new_node { - VNode::Suspended { real: new_real } => { - // - } - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), - VNode::Fragment(_) => todo!(), - VNode::Component(_) => todo!(), - } } + (VNode::Text(_), VNode::Element(_)) => { + self.create(new_node); + self.dom.replace_with(); + } + (VNode::Element(_), VNode::Text(_)) => { + self.create(new_node); + self.dom.replace_with(); + } + + _ => { + // + } /* + + */ + // + // VNode::Element(old) => match new_node { + // // New node is an element, old node was en element, need to investiage more deeply + // VNode::Element(new) => { + // // If the element type is completely different, the element needs to be re-rendered completely + // // This is an optimization React makes due to how users structure their code + // if new.tag_name != old.tag_name || new.namespace != old.namespace { + // self.create(new_node); + // self.dom.replace_with(); + // return; + // } + // new.dom_id.set(old.dom_id.get()); + + // self.diff_listeners(old.listeners, new.listeners); + // self.diff_attr(old.attributes, new.attributes, new.namespace); + // self.diff_children(old.children, new.children); + // } + // // New node is a text element, need to replace the element with a simple text node + // VNode::Text(_) => { + // self.create(new_node); + // self.dom.replace_with(); + // } + + // // New node is a component + // // Make the component and replace our element on the stack with it + // VNode::Component(_) => { + // self.create(new_node); + // self.dom.replace_with(); + // } + + // // New node is actually a sequence of nodes. + // // We need to replace this one node with a sequence of nodes + // // Not yet implement because it's kinda hairy + // VNode::Fragment(new) => { + // match new.children.len() { + // 0 => { + // // remove + // } + // 1 => { + // // replace + // self.create(&new.children[0]); + // self.dom.replace_with(); + // } + // _ => { + // todo!() + // // remove and mount many + // // self.remove_self_and_next_siblings(old) + // // + // // let iter = ChildIterator::new(new_node, self.components).rev(); + // // for child in iter {} + // // for child in new.children.iter().skip(1).rev() { + // // self.dom.remove(); + // // } + // } + // } + // } + + // // New Node is actually suspended. Todo + // VNode::Suspended { real } => todo!(), + // }, + + // // Old element was text + // VNode::Text(old) => match new_node { + // VNode::Text(new) => { + // if old.text != new.text { + // log::debug!("Text has changed {}, {}", old.text, new.text); + // self.dom.set_text(new.text); + // } + // new.dom_id.set(old.dom_id.get()); + // } + // VNode::Element(_) | VNode::Component(_) => { + // self.create(new_node); + // self.dom.replace_with(); + // } + + // // TODO on handling these types + // VNode::Fragment(frag) => { + // if frag.children.len() == 0 { + // // do nothing + // } else { + // self.create(&frag.children[0]); + // self.dom.replace_with(); + // for child in frag.children.iter().skip(1) { + // self.create(child); + // self.dom.append_child(); + // } + // } + // } + // VNode::Suspended { real } => todo!(), + // }, + + // // Old element was a component + // VNode::Component(old) => { + // match new_node { + // // It's something entirely different + // VNode::Element(_) | VNode::Text(_) => { + // self.create(new_node); + // self.dom.replace_with(); + // } + + // // It's also a component + // VNode::Component(new) => { + // if old.user_fc == new.user_fc { + // // Make sure we're dealing with the same component (by function pointer) + + // // Make sure the new component vnode is referencing the right scope id + // let scope_id = old.ass_scope.get(); + // new.ass_scope.set(scope_id); + // new.mounted_root.set(old.mounted_root.get()); + + // // make sure the component's caller function is up to date + // self.components + // .with_scope(scope_id.unwrap(), |scope| { + // scope.caller = Rc::downgrade(&new.caller) + // }) + // .unwrap(); + + // // React doesn't automatically memoize, but we do. + // // The cost is low enough to make it worth checking + // // Rust produces fairly performant comparison methods, sometimes SIMD accelerated + // let should_render = match old.comparator { + // Some(comparator) => comparator(new), + // None => true, + // }; + + // if should_render { + // // // self.dom.commit_traversal(); + // self.components + // .with_scope(scope_id.unwrap(), |f| { + // f.run_scope().unwrap(); + // }) + // .unwrap(); + // // diff_machine.change_list.load_known_root(root_id); + // // run the scope + // // + // } else { + // // Component has memoized itself and doesn't need to be re-rendered. + // // We still need to make sure the child's props are up-to-date. + // // Don't commit traversal + // } + // } else { + // // It's an entirely different component + + // // A new component has shown up! We need to destroy the old node + + // // Wipe the old one and plant the new one + // // self.dom.commit_traversal(); + // // self.dom.replace_node_with(old.dom_id, new.dom_id); + // // self.create(new_node); + // // self.dom.replace_with(); + // self.create(new_node); + // // self.create_and_repalce(new_node, old.mounted_root.get()); + + // // Now we need to remove the old scope and all of its descendents + // let old_scope = old.ass_scope.get().unwrap(); + // self.destroy_scopes(old_scope); + // } + // } + // VNode::Fragment(_) => todo!(), + // VNode::Suspended { real } => todo!(), + // } + // } + + // VNode::Fragment(old) => { + // // + // match new_node { + // VNode::Fragment(new) => todo!(), + + // // going from fragment to element means we're going from many (or potentially none) to one + // VNode::Element(new) => {} + // VNode::Text(_) => todo!(), + // VNode::Suspended { real } => todo!(), + // VNode::Component(_) => todo!(), + // } + // } + + // // a suspended node will perform a mem-copy of the previous elements until it is ready + // // this means that event listeners will need to be disabled and removed + // // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated + // VNode::Suspended { real: old_real } => { + // // + // match new_node { + // VNode::Suspended { real: new_real } => { + // // + // } + // VNode::Element(_) => todo!(), + // VNode::Text(_) => todo!(), + // VNode::Fragment(_) => todo!(), + // VNode::Component(_) => todo!(), + // } + // } } } @@ -340,6 +500,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // [... node] fn create(&mut self, node: &'bump VNode<'bump>) { // debug_assert!(self.dom.traversal_is_committed()); + log::warn!("Creating node!"); match node { VNode::Text(text) => { let real_id = self.dom.create_text_node(text.text); @@ -402,7 +563,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { } VNode::Component(component) => { - let real_id = self.dom.create_placeholder(); + // let real_id = self.dom.create_placeholder(); // let root_id = next_id(); // self.dom.save_known_root(root_id); @@ -455,6 +616,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { new_component.run_scope().unwrap(); // And then run the diff algorithm + let _real_id = self.dom.create_placeholder(); self.diff_node(new_component.old_frame(), new_component.next_frame()); // Finally, insert this node as a seen node. @@ -675,11 +837,14 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { ); if new_is_keyed && old_is_keyed { - todo!("Not yet implemented a migration away from temporaries"); + log::warn!("using the wrong approach"); + self.diff_non_keyed_children(old, new); + // todo!("Not yet implemented a migration away from temporaries"); // let t = self.dom.next_temporary(); // self.diff_keyed_children(old, new); // self.dom.set_next_temporary(t); } else { + log::debug!("diffing non keyed children"); self.diff_non_keyed_children(old, new); } } @@ -1120,24 +1285,24 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // self.dom.go_to_sibling(i); // [... parent this_child] - let did = old_child.get_mounted_id(self.components).unwrap(); - if did.0 == 0 { - log::debug!("Root is bad: {:#?}", old_child); - } - self.dom.push_root(did); + // let did = old_child.get_mounted_id(self.components).unwrap(); + // if did.0 == 0 { + // log::debug!("Root is bad: {:#?}", old_child); + // } + // self.dom.push_root(did); self.diff_node(old_child, new_child); - let old_id = old_child.get_mounted_id(self.components).unwrap(); - let new_id = new_child.get_mounted_id(self.components).unwrap(); + // let old_id = old_child.get_mounted_id(self.components).unwrap(); + // let new_id = new_child.get_mounted_id(self.components).unwrap(); - log::debug!( - "pushed root. {:?}, {:?}", - old_child.get_mounted_id(self.components).unwrap(), - new_child.get_mounted_id(self.components).unwrap() - ); - if old_id != new_id { - log::debug!("Mismatch: {:?}", new_child); - } + // log::debug!( + // "pushed root. {:?}, {:?}", + // old_child.get_mounted_id(self.components).unwrap(), + // new_child.get_mounted_id(self.components).unwrap() + // ); + // if old_id != new_id { + // log::debug!("Mismatch: {:?}", new_child); + // } } // match old.len().cmp(&new.len()) { @@ -1256,7 +1421,11 @@ enum KeyedPrefixResult { MoreWorkToDo(usize), } -struct ChildIterator<'a> { +/// This iterator iterates through a list of virtual children and only returns real children (Elements or Text). +/// +/// This iterator is useful when it's important to load the next real root onto the top of the stack for operations like +/// "InsertBefore". +struct RealChildIterator<'a> { scopes: &'a ScopeArena, // Heuristcally we should never bleed into 5 completely nested fragments/components @@ -1264,7 +1433,7 @@ struct ChildIterator<'a> { stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, } -impl<'a> ChildIterator<'a> { +impl<'a> RealChildIterator<'a> { fn new(starter: &'a VNode<'a>, scopes: &'a ScopeArena) -> Self { Self { scopes, @@ -1279,7 +1448,7 @@ impl<'a> ChildIterator<'a> { // } // } -impl<'a> Iterator for ChildIterator<'a> { +impl<'a> Iterator for RealChildIterator<'a> { type Item = &'a VNode<'a>; fn next(&mut self) -> Option<&'a VNode<'a>> { @@ -1402,7 +1571,7 @@ mod tests { let mut renderer = DebugDom::new(); dom.rebuild(&mut renderer).unwrap(); let starter = dom.base_scope().root(); - let ite = ChildIterator::new(starter, &dom.components); + let ite = RealChildIterator::new(starter, &dom.components); for child in ite { match child { VNode::Element(el) => println!("Found: Element {}", el.tag_name), diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index e7e963217..b551f2189 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -10,14 +10,62 @@ use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode}; #[derive(Debug)] pub struct EventTrigger { + /// pub component_id: ScopeIdx, + + /// pub real_node_id: RealDomNode, + + /// pub event: VirtualEvent, + + /// + pub priority: EventPriority, +} + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +#[derive(Debug)] +pub enum EventPriority { + /// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work + /// is typically reserved for small changes to single elements. + /// + /// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM. + Immediate, + + /// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work. + /// + /// + /// This is typically reserved for things like user interaction. + High, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take presedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + Medium, + + /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (suspense). + Low, } impl EventTrigger { - pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self { + pub fn new( + event: VirtualEvent, + scope: ScopeIdx, + mounted_dom_id: RealDomNode, + priority: EventPriority, + ) -> Self { Self { + priority, component_id: scope, real_node_id: mounted_dom_id, event, @@ -78,7 +126,7 @@ pub mod on { ( $( $eventdata:ident($wrapper:ident): [ $( $name:ident )* ]; )* ) => { $( #[derive(Debug)] - pub struct $wrapper(Rc); + pub struct $wrapper(pub Rc); impl Deref for $wrapper { type Target = Rc; fn deref(&self) -> &Self::Target { diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 619d860d0..095185c54 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -9,6 +9,8 @@ use std::{ u128, }; +use bumpalo::Bump; + use crate::{ events::VirtualEvent, innerlude::{Properties, VComponent, FC}, @@ -654,8 +656,9 @@ pub fn text3<'a>(bump: &'a bumpalo::Bump, args: std::fmt::Arguments) -> VNode<'a // we can just short-circuit to the &'static str version instead of having to allocate in the bump arena. // // In the most general case, this prevents the need for any string allocations for simple code IE: - // div {"abc"} - // + // ``` + // div {"abc"} + // ``` match args.as_str() { Some(static_str) => VNode::text(static_str), None => { @@ -735,13 +738,14 @@ impl<'a, 'b> ChildrenList<'a, 'b> { } } -// NodeFactory is used to build VNodes in the component's memory space. -// This struct adds metadata to the final VNode about listeners, attributes, and children +/// This struct provides an ergonomic API to quickly build VNodes. +/// +/// NodeFactory is used to build VNodes in the component's memory space. +/// This struct adds metadata to the final VNode about listeners, attributes, and children #[derive(Clone)] pub struct NodeFactory<'a> { pub scope_ref: &'a Scope, pub listener_id: Cell, - // pub listener_id: RefCell, } impl<'a> NodeFactory<'a> { @@ -756,7 +760,7 @@ impl<'a> NodeFactory<'a> { } /// Create an element builder - pub fn element<'b, Listeners, Attributes, Children>( + pub fn element<'b>( &'b self, tag_name: &'static str, ) -> ElementBuilder< @@ -787,6 +791,20 @@ impl<'a> NodeFactory<'a> { key: NodeKey::new_opt(key), })) } + + pub fn fragment_from_iter( + &self, + node_iter: impl IntoIterator>, + ) -> VNode<'a> { + let mut nodes = bumpalo::collections::Vec::new_in(self.bump()); + for node in node_iter.into_iter() { + nodes.push(node.into_vnode(&self)); + } + VNode::Fragment( + self.bump() + .alloc(VFragment::new(None, nodes.into_bump_slice())), + ) + } } use std::fmt::Debug; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a896236ea..3af83c052 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -29,7 +29,11 @@ pub enum VNode<'src> { /// A text node (node type `TEXT_NODE`). Text(VText<'src>), - /// A fragment is a "virtual position" in the DOM + /// A fragment is a list of elements that might have a dynamic order. + /// Normally, children will have a fixed order. However, Fragments allow a dynamic order and must be diffed differently. + /// + /// Fragments don't have a single mount into the dom, so their position is characterized by the head and tail nodes. + /// /// Fragments may have children and keys Fragment(&'src VFragment<'src>), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9241c99a8..8cc846323 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -487,6 +487,14 @@ impl Scope { self.caller = broken_caller; } + pub fn update_children<'creator_node>( + &mut self, + child_nodes: &'creator_node [VNode<'creator_node>], + ) { + let child_nodes = unsafe { std::mem::transmute(child_nodes) }; + self.child_nodes = child_nodes; + } + /// 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 /// @@ -597,9 +605,9 @@ impl Scope { /// /// } /// -/// fn example(cx: Context, props: &Props -> VNode { +/// fn example(cx: Context) -> VNode { /// html! { -///
"Hello, {cx.cx.name}"
+///
"Hello, {cx.name}"
/// } /// } /// ``` diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 4a979c42b..7a0338cb9 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT/Apache-2.0" [dependencies] dioxus-core = { path="../core", version="0.1.2" } js-sys = "0.3" -wasm-bindgen = "0.2.71" +wasm-bindgen = { version="0.2.71", features=["enable-interning"] } lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.20" wasm-logger = "0.2.0" @@ -31,6 +31,7 @@ slotmap = "1.0.3" version = "0.3.50" features = [ "Comment", + "Attr", "Document", "Element", "HtmlElement", @@ -47,6 +48,7 @@ features = [ "MouseEvent", "InputEvent", "ClipboardEvent", + "NamedNodeMap", "KeyboardEvent", "TouchEvent", "WheelEvent", @@ -70,4 +72,5 @@ crate-type = ["cdylib", "rlib"] [dev-dependencies] im-rc = "15.0.0" rand = { version="0.8.4", features=["small_rng"] } +separator = "0.4.1" uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] } diff --git a/packages/web/README.md b/packages/web/README.md index 0af6145f1..f2c8a9c75 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -3,3 +3,9 @@ Build interactive user experiences directly in the web browser! Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods. + + +## Web-specific Optimizations +- Uses string interning of all common node types +- Optimistically interns short strings +- Builds trees completely before mounting them diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 9cf52e0e0..1dc6e7f3e 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -1,5 +1,6 @@ //! Basic example that renders a simple VNode to the browser. +use dioxus::events::on::MouseEvent; use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_web::*; @@ -14,56 +15,42 @@ fn main() { } static App: FC<()> = |cx| { + let (state, set_state) = use_state(&cx, || 0); cx.render(rsx! { div { - h1 {"hello"} - C1 {} - C2 {} + section { class: "py-12 px-4 text-center" + div { class: "w-full max-w-2xl mx-auto" + span { class: "text-sm font-semibold" + "count: {state}" + } + div { + C1 { + onclick: move |_| set_state(state + 1) + "incr" + } + C1 { + onclick: move |_| set_state(state - 1) + "decr" + } + } + } + } } }) }; -static C1: FC<()> = |cx| { +#[derive(Props)] +struct IncrementerProps<'a> { + onclick: &'a dyn Fn(MouseEvent), +} + +fn C1<'a, 'b>(cx: Context<'a, IncrementerProps<'b>>) -> VNode<'a> { cx.render(rsx! { button { - "numba 1" + class: "inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow" + onclick: {cx.onclick} + "becr" + {cx.children()} } }) -}; - -static C2: FC<()> = |cx| { - cx.render(rsx! { - button { - "numba 2" - } - }) -}; - -static DocExamples: FC<()> = |cx| { - // - - let is_ready = false; - - let items = (0..10).map(|i| rsx! { li {"{i}"} }); - let _ = rsx! { - ul { - {items} - } - }; - - // rsx! { - // div {} - // h1 {} - // {""} - // "asbasd" - // dioxus::Fragment { - // // - // } - // } - - cx.render(rsx! { - div { - { is_ready.then(|| rsx!{ h1 {"We are ready!"} }) } - } - }) -}; +} diff --git a/packages/web/examples/calculator.rs b/packages/web/examples/calculator.rs new file mode 100644 index 000000000..bf1b8d554 --- /dev/null +++ b/packages/web/examples/calculator.rs @@ -0,0 +1,181 @@ +//! Example: Calculator +//! ------------------------- + +// use dioxus::events::on::*; +// use dioxus::prelude::*; + +use dioxus_core as dioxus; +use dioxus_web::WebsysRenderer; + +const STYLE: &str = include_str!("../../../examples/assets/calculator.css"); + +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)); +} + +use dioxus::events::on::*; +use dioxus::prelude::*; + +enum Operator { + Add, + Sub, + Mul, + Div, +} + +static App: FC<()> = |cx| { + let (cur_val, set_cur_val) = use_state(&cx, || 0.0_f64); + let (operator, set_operator) = use_state(&cx, || None as Option); + let (display_value, set_display_value) = use_state(&cx, || "".to_string()); + + let clear_display = display_value.eq("0"); + let clear_text = if clear_display { "C" } else { "AC" }; + + let input_digit = + move |num: u8| { + log::warn!("Inputting digit {:#?}", num); + let mut new = display_value.clone(); + new.push_str(num.to_string().as_str()); + set_display_value(new); + }; + + let input_dot = move || { + let mut new = display_value.clone(); + new.push_str("."); + set_display_value(new); + }; + + let perform_operation = move || { + if let Some(op) = operator.as_ref() { + let rhs = display_value.parse::().unwrap(); + let new_val = match op { + Operator::Add => *cur_val + rhs, + Operator::Sub => *cur_val - rhs, + Operator::Mul => *cur_val * rhs, + Operator::Div => *cur_val / rhs, + }; + set_cur_val(new_val); + set_display_value(new_val.to_string()); + set_operator(None); + } + }; + + let toggle_sign = move |_| { + if display_value.starts_with("-") { + set_display_value(display_value.trim_start_matches("-").to_string()) + } else { + set_display_value(format!("-{}", *display_value)) + } + }; + let toggle_percent = move |_| todo!(); + + let clear_key = move |_| { + set_display_value("0".to_string()); + if !clear_display { + set_operator(None); + set_cur_val(0.0); + } + }; + + let keydownhandler = move |evt: KeyboardEvent| match evt.key_code() { + KeyCode::Backspace => { + // + let mut new = display_value.clone(); + if !new.as_str().eq("0") { + new.pop(); + } + set_display_value(new); + + }, + KeyCode::_0 => input_digit(0), + KeyCode::_1 => input_digit(1), + KeyCode::_2 => input_digit(2), + KeyCode::_3 => input_digit(3), + KeyCode::_4 => input_digit(4), + KeyCode::_5 => input_digit(5), + KeyCode::_6 => input_digit(6), + KeyCode::_7 => input_digit(7), + KeyCode::_8 => input_digit(8), + KeyCode::_9 => input_digit(9), + KeyCode::Add => set_operator(Some(Operator::Add)), + KeyCode::Subtract => set_operator(Some(Operator::Sub)), + KeyCode::Divide => set_operator(Some(Operator::Div)), + KeyCode::Multiply => set_operator(Some(Operator::Mul)), + _ => {} + }; + + cx.render(rsx! { + div { class: "app" + onkeydown: {keydownhandler} + style { + "{STYLE}" + } + div { class: "calculator", + div { class: "input-keys" + CalculatorDisplay { val: *cur_val } + div { class: "function-keys" + CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"} + CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"} + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "●" } + + {(1..9).map(move |k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } + })} + } + div { class: "operator-keys" + CalculatorKey { name: "key-divide", onclick: move |_| set_operator(Some(Operator::Div)) "÷" } + CalculatorKey { name: "key-multiply", onclick: move |_| set_operator(Some(Operator::Mul)) "×" } + CalculatorKey { name: "key-subtract", onclick: move |_| set_operator(Some(Operator::Sub)) "−" } + CalculatorKey { name: "key-add", onclick: move |_| set_operator(Some(Operator::Add)) "+" } + CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" } + } + } + } + } + }) +}; + +#[derive(Props)] +struct CalculatorKeyProps<'a> { + /// Name! + name: &'static str, + + /// Click! + onclick: &'a dyn Fn(MouseEvent), +} + +fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { + cx.render(rsx! { + button { + class: "calculator-key {cx.name}" + onclick: {cx.onclick} + {cx.children()} + } + }) +} + +#[derive(Props, PartialEq)] +struct CalculatorDisplayProps { + val: f64, +} + +fn CalculatorDisplay(cx: Context) -> VNode { + use separator::Separatable; + // Todo, add float support to the num-format crate + let formatted = cx.val.separated_string(); + // TODO: make it autoscaling with css + cx.render(rsx! { + div { class: "calculator-display" + "{formatted}" + } + }) +} diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index 2b26a77c0..08749a751 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -3,15 +3,17 @@ use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; fn main() { - wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - console_error_panic_hook::set_once(); + // wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + // console_error_panic_hook::set_once(); + + // log::info!("hello world"); + dioxus_web::intern_cache(); - log::info!("hello world"); wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } static Example: FC<()> = |cx| { - let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"})); + let nodes = (0..500).map(|f| rsx! (li {"div"})); cx.render(rsx! { div { span { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index f6db01487..f5637d9f3 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -77,49 +77,6 @@ impl WebsysRenderer { .progress_with_event(&mut websys_dom, trigger)?; } - // 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()); - // self.internal_dom - // .progress_with_event(&mut websys_dom, event)?; - // 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 } } @@ -135,7 +92,7 @@ fn prepare_websys_dom() -> Element { // Build a dummy div let container: &Element = body.as_ref(); - container.set_inner_html(""); + // container.set_inner_html(""); container .append_child( document @@ -164,6 +121,299 @@ fn prepare_websys_dom() -> Element { // }; // } +/// Wasm-bindgen has a performance option to intern commonly used phrases +/// This saves the decoding cost, making the interaction of Rust<->JS more performant. +/// We intern all the HTML tags and attributes, making most operations much faster. +/// +/// Interning takes about 1ms at the start of the app, but saves a *ton* of time later on. +pub fn intern_cache() { + let cached_words = [ + // All the HTML Tags + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "big", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "command", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", + // All the event handlers + "Attribute", + "accept", + "accept-charset", + "accesskey", + "action", + "alt", + "async", + "autocomplete", + "autofocus", + "autoplay", + "charset", + "checked", + "cite", + "class", + "cols", + "colspan", + "content", + "contenteditable", + "controls", + "coords", + "data", + "data-*", + "datetime", + "default", + "defer", + "dir", + "dirname", + "disabled", + "download", + "draggable", + "enctype", + "for", + "form", + "formaction", + "headers", + "height", + "hidden", + "high", + "href", + "hreflang", + "http-equiv", + "id", + "ismap", + "kind", + "label", + "lang", + "list", + "loop", + "low", + "max", + "maxlength", + "media", + "method", + "min", + "multiple", + "muted", + "name", + "novalidate", + "onabort", + "onafterprint", + "onbeforeprint", + "onbeforeunload", + "onblur", + "oncanplay", + "oncanplaythrough", + "onchange", + "onclick", + "oncontextmenu", + "oncopy", + "oncuechange", + "oncut", + "ondblclick", + "ondrag", + "ondragend", + "ondragenter", + "ondragleave", + "ondragover", + "ondragstart", + "ondrop", + "ondurationchange", + "onemptied", + "onended", + "onerror", + "onfocus", + "onhashchange", + "oninput", + "oninvalid", + "onkeydown", + "onkeypress", + "onkeyup", + "onload", + "onloadeddata", + "onloadedmetadata", + "onloadstart", + "onmousedown", + "onmousemove", + "onmouseout", + "onmouseover", + "onmouseup", + "onmousewheel", + "onoffline", + "ononline", + "", + "onpageshow", + "onpaste", + "onpause", + "onplay", + "onplaying", + "", + "onprogress", + "onratechange", + "onreset", + "onresize", + "onscroll", + "onsearch", + "onseeked", + "onseeking", + "onselect", + "onstalled", + "", + "onsubmit", + "onsuspend", + "ontimeupdate", + "ontoggle", + "onunload", + "onvolumechange", + "onwaiting", + "onwheel", + "open", + "optimum", + "pattern", + "placeholder", + "poster", + "preload", + "readonly", + "rel", + "required", + "reversed", + "rows", + "rowspan", + "sandbox", + "scope", + "selected", + "shape", + "size", + "sizes", + "span", + "spellcheck", + "src", + "srcdoc", + "srclang", + "srcset", + "start", + "step", + "style", + "tabindex", + "target", + "title", + "translate", + "type", + "usemap", + "value", + "width", + "wrap", + ]; + + for s in cached_words { + wasm_bindgen::intern(s); + } +} + #[cfg(test)] mod tests { use std::env; diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index ec0700fa9..6e2eadfa8 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -171,6 +171,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { tag: &str, ns: Option<&'static str>, ) -> dioxus_core::virtual_dom::RealDomNode { + let tag = wasm_bindgen::intern(tag); let el = match ns { Some(ns) => self .document @@ -200,6 +201,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { el_id: usize, real_id: RealDomNode, ) { + let event = wasm_bindgen::intern(event); log::debug!( "Called [`new_event_listener`]: {}, {:?}, {}, {:?}", event, @@ -237,7 +239,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { // Instead, we just build and immediately execute a closure that returns result match decode_trigger(event) { Ok(synthetic_event) => trigger.as_ref()(synthetic_event), - Err(_) => log::error!("Error decoding Dioxus event attribute."), + Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), }; }) as Box); @@ -407,7 +409,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { #[derive(Debug)] pub struct CustomMouseEvent(web_sys::MouseEvent); - impl dioxus_core::events::on::MouseEvent for CustomMouseEvent { + impl dioxus_core::events::on::MouseEventInner for CustomMouseEvent { fn alt_key(&self) -> bool { self.0.alt_key() } @@ -451,7 +453,7 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { self.0.get_modifier_state(key_code) } } - VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt))) + VirtualEvent::MouseEvent(MouseEvent(Rc::new(CustomMouseEvent(evt)))) // MouseEvent(Box::new(RawMouseEvent { // alt_key: evt.alt_key(), // button: evt.button() as i32, @@ -547,25 +549,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { use anyhow::Context; + // for attr in { + let attrs = target.attributes(); + for x in 0..attrs.length() { + let attr = attrs.item(x).unwrap(); + log::debug!("attrs include: {:#?}", attr); + } + // } + // for attr in target.attributes() { + // log::debug!("attrs include: {:#?}", attr); + // } + // The error handling here is not very descriptive and needs to be replaced with a zero-cost error system let val: String = target .get_attribute(&format!("dioxus-event-{}", typ)) - .context("")?; + .context(format!("wrong format - received {:#?}", typ))?; let mut fields = val.splitn(3, "."); let gi_id = fields .next() .and_then(|f| f.parse::().ok()) - .context("")?; + .context("failed to parse gi id")?; let el_id = fields .next() .and_then(|f| f.parse::().ok()) - .context("")?; + .context("failed to parse el id")?; let real_id = fields .next() .and_then(|f| f.parse::().ok().map(RealDomNode::new)) - .context("")?; + .context("failed to parse real id")?; // Call the trigger log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id); @@ -575,6 +588,7 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { virtual_event_from_websys_event(event), triggered_scope, real_id, + dioxus_core::events::EventPriority::High, )) } @@ -584,3 +598,6 @@ impl ListenerMap { false } } + +struct JsStringCache {} +impl JsStringCache {}