diff --git a/examples/README.md b/examples/README.md index 8b8cd0911..8bf5b9dc4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,7 +11,7 @@ These examples are fully-fledged micro apps. They can be ran with the `cargo run | [Global State Management](./statemanagement.rs) | Share state between components | ๐Ÿ›  | | [Virtual Refs]() | Cross-platform imperative elements | ๐Ÿ›  | | [Inline Styles](./inline-styles.rs) | Define styles for elements inline | ๐Ÿ›  | -| [Conditional Rendering](./conditional-rendering.rs) | Hide/Show elements using conditionals | โœ… | +| [Conditional Rendering](./conditional-rendering.rs) | Hide/Show elements using conditionals | โœ… | These examples are not necessarily meant to be run, but rather serve as a reference for the given functionality. @@ -22,15 +22,15 @@ These examples are not necessarily meant to be run, but rather serve as a refere | [Global State Management](./statemanagement.rs) | Share state between components | ๐Ÿ›  | | [Virtual Refs]() | Cross-platform imperative elements | ๐Ÿ›  | | [Inline Styles](./inline-styles.rs) | Define styles for elements inline | ๐Ÿ›  | -| [Conditional Rendering](./conditional-rendering.rs) | Hide/Show elements using conditionals | โœ… | -| [Maps/Iterators](./iterators.rs) | Use iterators in the rsx! macro | ๐Ÿ›  | +| [Conditional Rendering](./conditional-rendering.rs) | Hide/Show elements using conditionals | โœ… | +| [Maps/Iterators](./iterators.rs) | Use iterators in the rsx! macro | โœ… | | [Render To string](./tostring.rs) | Render a mounted virtualdom to a string | ๐Ÿ›  | | [Component Children](./children.rs) | Pass children into child components | ๐Ÿ›  | | [Function Driven children]() | Pass functions to make VNodes | ๐Ÿ›  | -| [Memoization & Borrowed Data](./memo.rs) | Suppress renders, borrow from parents | โœ… | -| [Fragments](./fragments.rs) | Support root-less element groups | โœ… | +| [Memoization & Borrowed Data](./memo.rs) | Suppress renders, borrow from parents | โœ… | +| [Fragments](./fragments.rs) | Support root-less element groups | โœ… | | [Null/None Components](./empty.rs) | Return nothing! | ๐Ÿ›  | -| [Spread Pattern for props](./spreadpattern.rs) | Manually specify and override props | โœ… | +| [Spread Pattern for props](./spreadpattern.rs) | Manually specify and override props | โœ… | | [Controlled Inputs](./controlled-inputs.rs) | this does | ๐Ÿ›  | | [Custom Elements]() | Define custom elements | ๐Ÿ›  | | [Web Components]() | Custom elements to interface with WebComponents | ๐Ÿ›  | @@ -38,9 +38,9 @@ These examples are not necessarily meant to be run, but rather serve as a refere | [Asynchronous Data]() | Using suspense to wait for data | ๐Ÿ›  | | [Fiber/Scheduled Rendering]() | this does | ๐Ÿ›  | | [CSS Compiled Styles]() | this does | ๐Ÿ›  | -| [Anti-patterns](./antipatterns.rs) | A collection of discouraged patterns | โœ… | -| [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | โœ… | -| [Event Listeners](./listener.rs) | Attach closures to events on elements | โœ… | +| [Anti-patterns](./antipatterns.rs) | A collection of discouraged patterns | โœ… | +| [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | โœ… | +| [Event Listeners](./listener.rs) | Attach closures to events on elements | โœ… | These web-specific examples must be run with `dioxus-cli` using `dioxus develop --example XYZ` diff --git a/examples/calculator.rs b/examples/calculator.rs index b7bb911c2..0d21ffb76 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,7 +18,7 @@ enum Operator { Div, } -static App: FC<()> = |cx| { +const App: FC<()> = |cx| { let cur_val = use_state(cx, || 0.0_f64); let operator = use_state(cx, || None as Option); let display_value = use_state(cx, || "".to_string()); @@ -52,6 +52,7 @@ static App: FC<()> = |cx| { display_value.set(format!("-{}", *display_value)) } }; + let toggle_percent = move |_| todo!(); let clear_key = move |_| { diff --git a/examples/manually.rs b/examples/manually.rs new file mode 100644 index 000000000..b56c2a622 --- /dev/null +++ b/examples/manually.rs @@ -0,0 +1,23 @@ +use dioxus_core::*; + +fn main() { + use DomEdit::*; + + // .. should result in an "invalid node tree" + let edits = vec![ + CreateElement { tag: "div", id: 0 }, + // CreatePlaceholder { id: 1 }, + CreateElement { tag: "h1", id: 2 }, + CreateTextNode { + text: "hello world", + id: 3, + }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + // ReplaceWith { many: 1 }, + ]; + dioxus_webview::WebviewRenderer::run_with_edits(App, (), |c| c, Some(edits)).expect("failed"); +} + +const App: FC<()> = |cx| todo!(); diff --git a/examples/model.rs b/examples/model.rs index bd925a71a..9e7c12b53 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -28,129 +28,39 @@ fn main() { .expect("failed to launch dioxus app"); } -enum Operator { - Add, - Sub, - Mul, - Div, -} - static App: FC<()> = |cx| { - let (cur_val, set_cur_val) = use_state(cx, || 0.0_f64).classic(); - let (operator, set_operator) = use_state(cx, || None as Option).classic(); - let (display_value, set_display_value) = use_state(cx, || "0".to_string()).classic(); + let state = use_state(cx, || Calculator::new()); - let clear_display = display_value.eq("0"); + let clear_display = state.display_value.eq("0"); let clear_text = if clear_display { "C" } else { "AC" }; - - let input_digit = move |num: u8| { - let mut new = if operator.is_some() { - String::new() - } else if display_value == "0" { - String::new() - } else { - display_value.clone() - }; - if operator.is_some() { - let val = display_value.parse::().unwrap(); - set_cur_val(val); - } - - 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)), - _ => {} - }; + let formatted = state.formatted_display(); cx.render(rsx! { - div { - id: "wrapper" - div { class: "app" onkeydown: {keydownhandler} - style { "{STYLE}" } - div { class: "calculator", - CalculatorDisplay { val: &display_value} + div { id: "wrapper" + div { class: "app", style { "{STYLE}" } + div { class: "calculator", onkeypress: move |evt| state.get_mut().handle_keydown(evt), + div { class: "calculator-display", "{formatted}"} div { class: "calculator-keypad" div { class: "input-keys" 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} "%"} + CalculatorKey { name: "key-clear", onclick: move |_| state.get_mut().clear_display(), "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: move |_| state.get_mut().toggle_sign(), "ยฑ"} + CalculatorKey { name: "key-percent", onclick: move |_| state.get_mut().toggle_percent(), "%"} } div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "โ—" } + CalculatorKey { name: "key-0", onclick: move |_| state.get_mut().input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| state.get_mut().input_dot(), "โ—" } {(1..10).map(move |k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.get_mut().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() "=" } + CalculatorKey { name:"key-divide", onclick: move |_| state.get_mut().set_operator(Operator::Div), "รท" } + CalculatorKey { name:"key-multiply", onclick: move |_| state.get_mut().set_operator(Operator::Mul), "ร—" } + CalculatorKey { name:"key-subtract", onclick: move |_| state.get_mut().set_operator(Operator::Sub), "โˆ’" } + CalculatorKey { name:"key-add", onclick: move |_| state.get_mut().set_operator(Operator::Add), "+" } + CalculatorKey { name:"key-equals", onclick: move |_| state.get_mut().perform_operation(), "=" } } } } @@ -161,10 +71,7 @@ static App: FC<()> = |cx| { #[derive(Props)] struct CalculatorKeyProps<'a> { - /// Name! name: &'static str, - - /// Click! onclick: &'a dyn Fn(MouseEvent), } @@ -178,19 +85,106 @@ fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { }) } -#[derive(Props, PartialEq)] -struct CalculatorDisplayProps<'a> { - val: &'a str, +#[derive(Clone)] +struct Calculator { + display_value: String, + operator: Option, + waiting_for_operand: bool, + cur_val: f64, +} +#[derive(Clone)] +enum Operator { + Add, + Sub, + Mul, + Div, } -fn CalculatorDisplay<'a>(cx: Context<'a, CalculatorDisplayProps>) -> VNode<'a> { - use separator::Separatable; - // Todo, add float support to the num-format crate - let formatted = cx.val.parse::().unwrap().separated_string(); - // TODO: make it autoscaling with css - cx.render(rsx! { - div { class: "calculator-display" - div { class: "auto-scaling-text", "{formatted}" } +impl Calculator { + fn new() -> Self { + Calculator { + display_value: "0".to_string(), + operator: None, + waiting_for_operand: false, + cur_val: 0.0, } - }) + } + fn formatted_display(&self) -> String { + use separator::Separatable; + self.display_value + .parse::() + .unwrap() + .separated_string() + } + fn clear_display(&mut self) { + self.display_value = "0".to_string(); + } + fn input_digit(&mut self, digit: u8) { + let content = digit.to_string(); + if self.waiting_for_operand || self.display_value == "0" { + self.waiting_for_operand = false; + self.display_value = content; + } else { + self.display_value.push_str(content.as_str()); + } + } + fn input_dot(&mut self) { + if self.display_value.find(".").is_none() { + self.display_value.push_str("."); + } + } + fn perform_operation(&mut self) { + if let Some(op) = &self.operator { + let rhs = self.display_value.parse::().unwrap(); + let new_val = match op { + Operator::Add => self.cur_val + rhs, + Operator::Sub => self.cur_val - rhs, + Operator::Mul => self.cur_val * rhs, + Operator::Div => self.cur_val / rhs, + }; + self.cur_val = new_val; + self.display_value = new_val.to_string(); + self.operator = None; + } + } + fn toggle_sign(&mut self) { + if self.display_value.starts_with("-") { + self.display_value = self.display_value.trim_start_matches("-").to_string(); + } else { + self.display_value = format!("-{}", self.display_value); + } + } + fn toggle_percent(&mut self) { + self.display_value = (self.display_value.parse::().unwrap() / 100.0).to_string(); + } + fn backspace(&mut self) { + if !self.display_value.as_str().eq("0") { + self.display_value.pop(); + } + } + fn set_operator(&mut self, operator: Operator) { + self.operator = Some(operator); + self.cur_val = self.display_value.parse::().unwrap(); + self.waiting_for_operand = true; + } + fn handle_keydown(&mut self, evt: KeyboardEvent) { + match evt.key_code() { + KeyCode::Backspace => self.backspace(), + KeyCode::_0 => self.input_digit(0), + KeyCode::_1 => self.input_digit(1), + KeyCode::_2 => self.input_digit(2), + KeyCode::_3 => self.input_digit(3), + KeyCode::_4 => self.input_digit(4), + KeyCode::_5 => self.input_digit(5), + KeyCode::_6 => self.input_digit(6), + KeyCode::_7 => self.input_digit(7), + KeyCode::_8 => self.input_digit(8), + KeyCode::_9 => self.input_digit(9), + KeyCode::Add => self.operator = Some(Operator::Add), + KeyCode::Subtract => self.operator = Some(Operator::Sub), + KeyCode::Divide => self.operator = Some(Operator::Div), + KeyCode::Multiply => self.operator = Some(Operator::Mul), + _ => {} + } + } } diff --git a/examples/reference/README.md b/examples/reference/README.md index 96f38e9e7..7ef471b49 100644 --- a/examples/reference/README.md +++ b/examples/reference/README.md @@ -21,7 +21,6 @@ This folder holds a bunch of useful reference code. However, none of the code is | [NodeRef](./noderefs.rs) | How to handle futures with suspense | | [Signals](./signals.rs) | How to handle futures with suspense | | [ToString](./tostring.rs) | How to handle futures with suspense | -| [Fiber](./fiber.rs) | How to handle futures with suspense | | [Global CSS](./global_css.rs) | How to handle futures with suspense | | [State Management](./statemanagement.rs) | How to handle futures with suspense | | [Testing](./testing.rs) | How to handle futures with suspense | diff --git a/examples/reference/empty.rs b/examples/reference/empty.rs index 7afcd0f9e..21a171fed 100644 --- a/examples/reference/empty.rs +++ b/examples/reference/empty.rs @@ -4,5 +4,7 @@ //! This is a simple pattern that allows you to return no elements! fn main() {} + use dioxus::prelude::*; + static Example: FC<()> = |cx| cx.render(rsx! { Fragment {} }); diff --git a/examples/reference/fiber.rs b/examples/reference/fiber.rs deleted file mode 100644 index 096f0d9e0..000000000 --- a/examples/reference/fiber.rs +++ /dev/null @@ -1,10 +0,0 @@ -use dioxus::prelude::*; -fn main() {} - -static Example: FC<()> = |cx| { - cx.render(rsx! { - div { - - } - }) -}; diff --git a/examples/reference/global_css.rs b/examples/reference/global_css.rs index 096f0d9e0..87b02d6f1 100644 --- a/examples/reference/global_css.rs +++ b/examples/reference/global_css.rs @@ -1,10 +1,31 @@ +//! Examples: CSS +//! ------------- +//! +//! Originally taken from: +//! - https://www.w3schools.com/html/tryit.asp?filename=tryhtml_css_internal +//! +//! Include global styles in your app! +//! +//! You can simply drop in a "style" tag and set the inner contents to your stylesheet. +//! It's slightly more manual than React, but is less magical. +//! +//! A coming update with the assets system will make it possible to include global css from child components. + use dioxus::prelude::*; fn main() {} -static Example: FC<()> = |cx| { - cx.render(rsx! { - div { +const STYLE: &str = r#" +body {background-color: powderblue;} +h1 {color: blue;} +p {color: red;} +"#; +const Example: FC<()> = |cx| { + cx.render(rsx! { + head { style { "{STYLE}" } } + body { + h1 {"This is a heading"} + p {"This is a paragraph"} } }) }; diff --git a/examples/reference/iterators.rs b/examples/reference/iterators.rs index 53591b41a..c5c205013 100644 --- a/examples/reference/iterators.rs +++ b/examples/reference/iterators.rs @@ -1,17 +1,38 @@ -use dioxus::prelude::*; +//! Example: Iterators +//! ------------------ +//! +//! This example demonstrates how to use iterators with Dioxus. +//! Iterators must be used through the curly braces item in element bodies. +//! While you might be inclined to `.collect::<>` into Html, Dioxus prefers you provide an iterator that +//! resolves to VNodes. It's more efficient and easier to write than having to `collect` everywhere. +//! +//! This also makes it easy to write "pull"-style iterators that don't have a known size. + fn main() {} +use dioxus::prelude::*; static Example: FC<()> = |cx| { let g = use_state(cx, || 0); + let v = (0..10).map(|f| { rsx! { li { - onclick: move |_| g.set(10) + onclick: move |_| g.set(f) + "ID: {f}" + ul { + {(0..10).map(|k| rsx!{ + li { + "Sub iterator: {f}.{k}" + } + })} + } } } }); + cx.render(rsx! { - div { + h3 {"Selected: {g}"} + ul { {v} } }) diff --git a/examples/showcase.rs b/examples/showcase.rs index 7fb2db260..ae1b9f08b 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -11,7 +11,6 @@ mod reference { mod controlled_inputs; mod custom_elements; mod empty; - mod fiber; mod fragments; mod global_css; mod inline_styles; diff --git a/examples/testbed.rs b/examples/testbed.rs index c99fef627..97c6b158b 100644 --- a/examples/testbed.rs +++ b/examples/testbed.rs @@ -1,15 +1,28 @@ use dioxus::prelude::*; -fn main() {} +fn main() { + env_logger::init(); + dioxus::desktop::launch(Example, |c| c); +} -static App: FC<()> = |cx| { - // - cx.render(rsx!( - div { - h1 {} - } - )) +const STYLE: &str = r#" +body {background-color: powderblue;} +h1 {color: blue;} +p {color: red;} +"#; + +const Example: FC<()> = |cx| { + cx.render(rsx! { + Child { } + Child { } + }) }; -#[test] -fn blah() {} +const Child: FC<()> = |cx| { + cx.render(rsx!( + h1 {"1" } + h1 {"2" } + h1 {"3" } + h1 {"4" } + )) +}; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 3d258273f..eaa9ba214 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -37,6 +37,8 @@ slotmap = "1.0.3" appendlist = "1.4.0" futures-util = "0.3.15" +smallvec = "1.6.1" + [features] default = [] diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs index fa738335c..875bf6ec6 100644 --- a/packages/core/src/childiter.rs +++ b/packages/core/src/childiter.rs @@ -19,12 +19,6 @@ impl<'a> RealChildIterator<'a> { } } -// impl<'a> DoubleEndedIterator for ChildIterator<'a> { -// fn next_back(&mut self) -> Option { -// todo!() -// } -// } - impl<'a> Iterator for RealChildIterator<'a> { type Item = &'a VNode<'a>; @@ -37,6 +31,7 @@ impl<'a> Iterator for RealChildIterator<'a> { if let Some((count, node)) = self.stack.last_mut() { match node { // We can only exit our looping when we get "real" nodes + // This includes fragments and components when they're empty (have a single root) VNode::Element(_) | VNode::Text(_) => { // We've recursed INTO an element/text // We need to recurse *out* of it and move forward to the next @@ -46,11 +41,17 @@ impl<'a> Iterator for RealChildIterator<'a> { // If we get a fragment we push the next child VNode::Fragment(frag) => { - let _count = *count as usize; - if _count >= frag.children.len() { + let subcount = *count as usize; + + if frag.children.len() == 0 { + should_pop = true; + returned_node = Some(&**node); + } + + if subcount >= frag.children.len() { should_pop = true; } else { - should_push = Some(&frag.children[_count]); + should_push = Some(&frag.children[subcount]); } } @@ -88,77 +89,3 @@ impl<'a> Iterator for RealChildIterator<'a> { returned_node } } - -mod tests { - use super::*; - use crate as dioxus; - use crate::innerlude::*; - use crate::util::DebugDom; - use dioxus_core_macro::*; - - // #[test] - // fn test_child_iterator() { - // static App: FC<()> = |cx| { - // cx.render(rsx! { - // Fragment { - // div {} - // h1 {} - // h2 {} - // h3 {} - // Fragment { - // "internal node" - // div { - // "baller text shouldn't show up" - // } - // p { - - // } - // Fragment { - // Fragment { - // "wow you really like framgents" - // Fragment { - // "why are you like this" - // Fragment { - // "just stop now please" - // Fragment { - // "this hurts" - // Fragment { - // "who needs this many fragments?????" - // Fragment { - // "just... fine..." - // Fragment { - // "no" - // } - // } - // } - // } - // } - // } - // } - // } - // } - // "my text node 1" - // "my text node 2" - // "my text node 3" - // "my text node 4" - // } - // }) - // }; - // let mut dom = VirtualDom::new(App); - // let mut renderer = DebugDom::new(); - // dom.rebuild(&mut renderer).unwrap(); - // let starter = dom.base_scope().root(); - // let ite = RealChildIterator::new(starter, &dom.components); - // for child in ite { - // match child { - // VNode::Element(el) => println!("Found: Element {}", el.tag_name), - // VNode::Text(t) => println!("Found: Text {:?}", t.text), - - // // These would represent failing cases. - // VNode::Fragment(_) => panic!("Found: Fragment"), - // VNode::Suspended { real } => panic!("Found: Suspended"), - // VNode::Component(_) => panic!("Found: Component"), - // } - // } - // } -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index e989c4e37..7bb5ea69f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,16 +1,14 @@ //! This module contains the stateful DiffMachine and all methods to diff VNodes, their properties, and their children. //! The DiffMachine calculates the diffs between the old and new frames, updates the new nodes, and modifies the real dom. //! -//! Notice: -//! ------ -//! +//! ## Notice: //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support //! Components, Fragments, Suspense, SubTree memoization, and additional batching operations. //! //! ## Implementation Details: //! //! ### IDs for elements -//! +//! -------------------- //! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. //! We don't necessarily require that DOM changes happen instnatly during the diffing process, so the implementor may choose //! to batch nodes if it is more performant for their application. The expectation is that renderers use a Slotmap for nodes @@ -21,6 +19,7 @@ //! brick the user's page. //! //! ## Subtree Memoization +//! ----------------------- //! We also employ "subtree memoization" which saves us from having to check trees which take no dynamic content. We can //! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the //! calls to "create" propogate this information upwards. Structures like the one below are entirely static: @@ -30,11 +29,21 @@ //! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so its up to the reconciler to //! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP //! +//! ## Bloom Filter and Heuristics +//! ------------------------------ +//! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are +//! currently very rough, but will get better as time goes on. For FFI, we recommend using a bloom filter to cache strings. +//! +//! ## Garbage Collection +//! --------------------- +//! We roughly place the role of garbage collection onto the reconciler. Dioxus needs to manage the lifecycle of components +//! but will not spend any time cleaning up old elements. It's the Reconciler's duty to understand which elements need to +//! be cleaned up *after* the diffing is completed. The reconciler should schedule this garbage collection as the absolute +//! lowest priority task, after all edits have been applied. //! //! //! Further Reading and Thoughts //! ---------------------------- -//! //! There are more ways of increasing diff performance here that are currently not implemented. //! More info on how to improve this diffing algorithm: //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ @@ -58,11 +67,14 @@ use std::any::Any; pub trait RealDom<'a> { // Navigation fn push_root(&mut self, root: RealDomNode); + fn pop(&mut self); // Add Nodes to the dom // add m nodes from the stack fn append_children(&mut self, many: u32); + // replace the n-m node on the stack with the m nodes + // ends with the last element of the chain on the top of the stack fn replace_with(&mut self, many: u32); // Remove Nodesfrom the dom @@ -138,23 +150,14 @@ where seen_nodes: FxHashSet::default(), } } + // Diff the `old` node with the `new` node. Emits instructions to modify a // physical DOM node that reflects `old` into something that reflects `new`. // - // Upon entry to this function, the physical DOM node must be on the top of the - // change list stack: + // the real stack should be what it is coming in and out of this function (ideally empty) // - // [... node] - // - // The change list stack is in the same state when this function exits. - // In the case of Fragments, the parent node is on the stack + // each function call assumes the stack is fresh (empty). pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - /* - For each valid case, we "commit traversal", meaning we save this current position in the tree. - 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 - */ - // 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". @@ -168,7 +171,9 @@ where self.dom.push_root(old.dom_id.get()); log::debug!("Text has changed {}, {}", old.text, new.text); self.dom.set_text(new.text); + self.dom.pop(); } + new.dom_id.set(old.dom_id.get()); } @@ -181,13 +186,19 @@ where self.dom.push_root(old.dom_id.get()); let meta = self.create(new_node); self.dom.replace_with(meta.added_to_stack); + self.dom.pop(); return; } - new.dom_id.set(old.dom_id.get()); + let oldid = old.dom_id.get(); + new.dom_id.set(oldid); + + // push it just in case + self.dom.push_root(oldid); self.diff_listeners(old.listeners, new.listeners); self.diff_attr(old.attributes, new.attributes, new.namespace); self.diff_children(old.children, new.children); + self.dom.pop(); } (VNode::Component(old), VNode::Component(new)) => { @@ -278,44 +289,47 @@ where 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. + // The strategy here is to pick the first possible node from the previous set and use that as our replace with root + // We also walk the "real node" list to make sure all latent roots are claened up + // This covers the case any time a fragment or component shows up with pretty much anything else + ( + VNode::Component(_) | VNode::Fragment(_) | VNode::Text(_) | VNode::Element(_), + VNode::Component(_) | VNode::Fragment(_) | VNode::Text(_) | VNode::Element(_), + ) => { + // Choose the node to use as the placeholder for replacewith + let back_node = match old_node { + VNode::Element(_) | VNode::Text(_) => old_node + .get_mounted_id(&self.components) + .expect("Element and text always have a real node"), - // 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)) => { - // - } - (VNode::Text(_), VNode::Element(_)) => { - self.create(new_node); - self.dom.replace_with(1); - } - (VNode::Element(_), VNode::Text(_)) => { - self.create(new_node); - self.dom.replace_with(1); + _ => { + let mut old_iter = RealChildIterator::new(old_node, &self.components); + + let back_node = old_iter + .next() + .expect("Empty fragments should generate a placeholder."); + + // remove any leftovers + for to_remove in old_iter { + self.dom.push_root(to_remove); + self.dom.remove(); + } + + back_node + } + }; + + // replace the placeholder or first node with the nodes generated from the "new" + self.dom.push_root(back_node); + let meta = self.create(new_node); + self.dom.replace_with(meta.added_to_stack); } - _ => { - // - } + // TODO + (VNode::Suspended { .. }, _) => todo!(), + (_, VNode::Suspended { .. }) => todo!(), } } } @@ -480,23 +494,19 @@ where // Run the scope for one iteration to initialize it new_component.run_scope().unwrap(); - // By using "diff_node" instead of "create", we delegate the mutations to the child - // However, "diff_node" always expects a real node on the stack, so we put a placeholder so it knows where to start. - // // TODO: we need to delete (IE relcaim this node, otherwise the arena will grow infinitely) - let _ = self.dom.create_placeholder(); - self.diff_node(new_component.old_frame(), new_component.next_frame()); + let nextnode = new_component.next_frame(); + let meta = self.create(nextnode); // Finally, insert this node as a seen node. self.seen_nodes.insert(idx); - // Virtual Components don't result in new nodes on the stack - // However, we can skip them from future diffing if they take no children, have no props, take no key, etc. - CreateMeta::new(vcomponent.is_static, 0) + CreateMeta::new(vcomponent.is_static, meta.added_to_stack) } // Fragments are the only nodes that can contain dynamic content (IE through curlies or iterators). // We can never ignore their contents, so the prescence of a fragment indicates that we need always diff them. + // Fragments will just put all their nodes onto the stack after creation VNode::Fragment(frag) => { let mut nodes_added = 0; for child in frag.children.iter().rev() { @@ -506,6 +516,7 @@ where let new_meta = self.create(child); nodes_added += new_meta.added_to_stack; } + log::info!("This fragment added {} nodes to the stack", nodes_added); // Never ignore CreateMeta::new(false, nodes_added) @@ -877,6 +888,39 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } + // Remove all of a node's children. + // + // The change list stack must have this shape upon entry to this function: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + pub fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { + // debug_assert!(self.dom.traversal_is_committed()); + log::debug!("REMOVING CHILDREN"); + for _child in old { + // registry.remove_subtree(child); + } + // Fast way to remove all children: set the node's textContent to an empty + // string. + todo!() + // self.dom.set_inner_text(""); + } + + // Create the given children and append them to the parent node. + // + // The parent node must currently be on top of the change list stack: + // + // [... parent] + // + // When this function returns, the change list stack is in the same state. + pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { + for child in new { + let meta = self.create(child); + self.dom.append_children(meta.added_to_stack); + } + } + // The most-general, expensive code path for keyed children diffing. // // We find the longest subsequence within `old` of children that are relatively @@ -1217,39 +1261,6 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // Support methods // ====================== - // Remove all of a node's children. - // - // The change list stack must have this shape upon entry to this function: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn remove_all_children(&mut self, old: &'bump [VNode<'bump>]) { - // debug_assert!(self.dom.traversal_is_committed()); - log::debug!("REMOVING CHILDREN"); - for _child in old { - // registry.remove_subtree(child); - } - // Fast way to remove all children: set the node's textContent to an empty - // string. - todo!() - // self.dom.set_inner_text(""); - } - - // Create the given children and append them to the parent node. - // - // The parent node must currently be on top of the change list stack: - // - // [... parent] - // - // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &'bump [VNode<'bump>]) { - for child in new { - let meta = self.create(child); - self.dom.append_children(meta.added_to_stack); - } - } - // Remove the current child and all of its following siblings. // // The change list stack must have this shape upon entry to this function: @@ -1298,3 +1309,95 @@ enum KeyedPrefixResult { // the beginning of `new` and `old` we already processed. MoreWorkToDo(usize), } + +/// 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 SharedArena, + + // Heuristcally we should never bleed into 5 completely nested fragments/components + // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane + stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, +} + +impl<'a> RealChildIterator<'a> { + fn new(starter: &'a VNode<'a>, scopes: &'a SharedArena) -> Self { + Self { + scopes, + stack: smallvec::smallvec![(0, starter)], + } + } +} + +impl<'a> Iterator for RealChildIterator<'a> { + type Item = RealDomNode; + + fn next(&mut self) -> Option { + let mut should_pop = false; + let mut returned_node = None; + let mut should_push = None; + + while returned_node.is_none() { + if let Some((count, node)) = self.stack.last_mut() { + match node { + // We can only exit our looping when we get "real" nodes + // This includes fragments and components when they're empty (have a single root) + VNode::Element(_) | VNode::Text(_) => { + // We've recursed INTO an element/text + // We need to recurse *out* of it and move forward to the next + should_pop = true; + returned_node = node.get_mounted_id(&self.scopes); + } + + // If we get a fragment we push the next child + VNode::Fragment(frag) => { + let subcount = *count as usize; + + if frag.children.len() == 0 { + should_pop = true; + returned_node = node.get_mounted_id(&self.scopes); + } + + if subcount >= frag.children.len() { + should_pop = true; + } else { + should_push = Some(&frag.children[subcount]); + } + } + + // Immediately abort suspended nodes - can't do anything with them yet + // VNode::Suspended => should_pop = true, + VNode::Suspended { real } => todo!(), + + // For components, we load their root and push them onto the stack + VNode::Component(sc) => { + let scope = self.scopes.try_get(sc.ass_scope.get().unwrap()).unwrap(); + + // Simply swap the current node on the stack with the root of the component + *node = scope.root(); + } + } + } else { + // If there's no more items on the stack, we're done! + return None; + } + + if should_pop { + self.stack.pop(); + if let Some((id, _)) = self.stack.last_mut() { + *id += 1; + } + should_pop = false; + } + + if let Some(push) = should_push { + self.stack.push((0, push)); + should_push = None; + } + } + + returned_node + } +} diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index e81a2954c..b0ecbbfca 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -693,10 +693,11 @@ impl<'a> NodeFactory<'a> { VNode::Fragment(self.bump().alloc(VFragment { children, key: NodeKey::new_opt(key), + void_root: Cell::new(None), })) } - pub fn virtual_child( + pub fn virtual_child( &self, f: FC, props: T, @@ -705,6 +706,7 @@ impl<'a> NodeFactory<'a> { ) -> VNode<'a> where C: 'a + AsRef<[VNode<'a>]>, + T: Properties + 'a, { let children: &'a C = self.bump().alloc(children); VNode::Component(self.bump().alloc(crate::nodes::VComponent::new( diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 991b85897..9aaffec1a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -182,9 +182,9 @@ impl<'a> VNode<'a> { match self { VNode::Element(el) => Some(el.dom_id.get()), VNode::Text(te) => Some(te.dom_id.get()), - VNode::Fragment(_) => todo!(), + VNode::Fragment(frag) => frag.void_root.get(), VNode::Suspended { .. } => todo!(), - VNode::Component(el) => Some(el.mounted_root.get()), + VNode::Component(el) => todo!(), } } } @@ -341,8 +341,7 @@ pub type VCompAssociatedScope = Option; pub struct VComponent<'src> { pub key: NodeKey<'src>, - pub mounted_root: Cell, - + // pub void_root: Cell>, pub ass_scope: Cell, // todo: swap the RC out with @@ -438,7 +437,7 @@ impl<'a> VComponent<'a> { key, caller, is_static, - mounted_root: Cell::new(RealDomNode::empty()), + // void_root: Cell::new(None), } } } @@ -476,6 +475,7 @@ pub fn create_component_caller<'a, P: 'a>( pub struct VFragment<'src> { pub key: NodeKey<'src>, pub children: &'src [VNode<'src>], + pub void_root: Cell>, } impl<'a> VFragment<'a> { @@ -485,6 +485,10 @@ impl<'a> VFragment<'a> { None => NodeKey(None), }; - Self { key, children } + Self { + key, + children, + void_root: Cell::new(None), + } } } diff --git a/packages/core/src/serialize.rs b/packages/core/src/serialize.rs index daa468cf8..e28c6ae24 100644 --- a/packages/core/src/serialize.rs +++ b/packages/core/src/serialize.rs @@ -18,8 +18,12 @@ pub enum DomEdit<'bump> { PushRoot { root: u64, }, - AppendChild, - ReplaceWith, + AppendChildren { + many: u32, + }, + ReplaceWith { + many: u32, + }, Remove, RemoveAllChildren, CreateTextNode { diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 4ae22a923..6be46c476 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -87,6 +87,7 @@ impl DebugDom { } impl<'a> RealDom<'a> for DebugDom { fn push_root(&mut self, root: RealDomNode) {} + fn pop(&mut self) {} fn append_children(&mut self, many: u32) {} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ea86c27ff..83b2c0502 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -152,8 +152,6 @@ impl VirtualDom { }) .unwrap(); - log::debug!("base scope is {:#?}", base_scope); - Self { base_scope, event_queue, @@ -318,12 +316,6 @@ impl VirtualDom { let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); diff_machine.diff_node(old, new); - - // log::debug!( - // "Processing update: {:#?} with height {}", - // &update.idx, - // cur_height - // ); } Ok(()) diff --git a/packages/webview/src/blah.html b/packages/webview/src/blah.html deleted file mode 100644 index a02ce6398..000000000 --- a/packages/webview/src/blah.html +++ /dev/null @@ -1,99 +0,0 @@ -class OPTABLE { -PushRoot(self, edit) { -const id = edit.root; -const node = self.nodes[id]; -self.stack.push(node); -} -AppendChild(self, edit) { -// todo: prevent merging of text nodes -const node = self.pop(); -self.top().appendChild(node); -} -ReplaceWith(self, edit) { -const newNode = self.pop(); -const oldNode = self.pop(); -oldNode.replaceWith(newNode); -self.stack.push(newNode); -} -Remove(self, edit) { -const node = self.stack.pop(); -node.remove(); -} -RemoveAllChildren(self, edit) { -// todo - we never actually call this one -} -CreateTextNode(self, edit) { -self.stack.push(document.createTextNode(edit.text)); -} -CreateElement(self, edit) { -const tagName = edit.tag; -console.log(`creating element! ${edit}`); -self.stack.push(document.createElement(tagName)); -} -CreateElementNs(self, edit) { -self.stack.push(document.createElementNS(edit.ns, edit.tag)); -} -CreatePlaceholder(self, edit) { -self.stack.push(document.createElement("pre")); -} -NewEventListener(self, edit) { -// todo -} -RemoveEventListener(self, edit) { -// todo -} -SetText(self, edit) { -self.top().textContent = edit.text; -} -SetAttribute(self, edit) { -const name = edit.field; -const value = edit.value; -const node = self.top(self.stack); -node.setAttribute(name, value); - -// Some attributes are "volatile" and don't work through `setAttribute`. -if ((name === "value", self)) { -node.value = value; -} -if ((name === "checked", self)) { -node.checked = true; -} -if ((name === "selected", self)) { -node.selected = true; -} -} -RemoveAttribute(self, edit) { -const name = edit.field; -const node = self.top(self.stack); -node.removeAttribute(name); - -// Some attributes are "volatile" and don't work through `removeAttribute`. -if ((name === "value", self)) { -node.value = null; -} -if ((name === "checked", self)) { -node.checked = false; -} -if ((name === "selected", self)) { -node.selected = false; -} -} -} - -// const op_table = new OPTABLE(); -// const interpreter = new Interpreter(window.document.body); - -// function EditListReceived(rawEditList) { -// let editList = JSON.parse(rawEditList); -// console.warn("hnelllo"); -// editList.forEach(function (edit, index) { -// console.log(edit); -// op_table[edit.type](interpreter, edit); -// }); -// } - -// async function rinalize() { -// console.log("initialize..."); -// let edits = await rpc.call("initiate"); -// console.error(edits); -// } diff --git a/packages/webview/src/blah.js b/packages/webview/src/blah.js deleted file mode 100644 index 062b8596d..000000000 --- a/packages/webview/src/blah.js +++ /dev/null @@ -1,81 +0,0 @@ -class OPTABLE { - PushRoot(self, edit) { - const id = edit.root; - const node = self.nodes[id]; - self.stack.push(node); - } - AppendChild(self, edit) { - // todo: prevent merging of text nodes - const node = self.pop(); - self.top().appendChild(node); - } - ReplaceWith(self, edit) { - const newNode = self.pop(); - const oldNode = self.pop(); - oldNode.replaceWith(newNode); - self.stack.push(newNode); - } - Remove(self, edit) { - const node = self.stack.pop(); - node.remove(); - } - RemoveAllChildren(self, edit) { - // todo - we never actually call this one - } - CreateTextNode(self, edit) { - self.stack.push(document.createTextNode(edit.text)); - } - CreateElement(self, edit) { - const tagName = edit.tag; - console.log(`creating element! ${edit}`); - self.stack.push(document.createElement(tagName)); - } - CreateElementNs(self, edit) { - self.stack.push(document.createElementNS(edit.ns, edit.tag)); - } - CreatePlaceholder(self, edit) { - self.stack.push(document.createElement("pre")); - } - NewEventListener(self, edit) { - // todo - } - RemoveEventListener(self, edit) { - // todo - } - SetText(self, edit) { - self.top().textContent = edit.text; - } - SetAttribute(self, edit) { - const name = edit.field; - const value = edit.value; - const node = self.top(self.stack); - node.setAttribute(name, value); - - // Some attributes are "volatile" and don't work through `setAttribute`. - if ((name === "value", self)) { - node.value = value; - } - if ((name === "checked", self)) { - node.checked = true; - } - if ((name === "selected", self)) { - node.selected = true; - } - } - RemoveAttribute(self, edit) { - const name = edit.field; - const node = self.top(self.stack); - node.removeAttribute(name); - - // Some attributes are "volatile" and don't work through `removeAttribute`. - if ((name === "value", self)) { - node.value = null; - } - if ((name === "checked", self)) { - node.checked = false; - } - if ((name === "selected", self)) { - node.selected = false; - } - } -} diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index 38bfd3c43..79f7deae4 100644 --- a/packages/webview/src/dom.rs +++ b/packages/webview/src/dom.rs @@ -36,11 +36,11 @@ impl<'bump> RealDom<'bump> for WebviewDom<'bump> { } fn append_children(&mut self, many: u32) { - self.edits.push(AppendChild); + self.edits.push(AppendChildren { many }); } fn replace_with(&mut self, many: u32) { - self.edits.push(ReplaceWith); + self.edits.push(ReplaceWith { many }); } fn remove(&mut self) { diff --git a/packages/webview/src/index.html b/packages/webview/src/index.html index f70077581..7513ec41e 100644 --- a/packages/webview/src/index.html +++ b/packages/webview/src/index.html @@ -3,136 +3,149 @@ + + + + -
+
+ _ +
- diff --git a/packages/webview/src/lib.rs b/packages/webview/src/lib.rs index f17aa4ab9..674236a3b 100644 --- a/packages/webview/src/lib.rs +++ b/packages/webview/src/lib.rs @@ -47,6 +47,15 @@ impl WebviewRenderer { root: FC, props: T, user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + ) -> anyhow::Result<()> { + Self::run_with_edits(root, props, user_builder, None) + } + + pub fn run_with_edits( + root: FC, + props: T, + user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder, + redits: Option>>, ) -> anyhow::Result<()> { use wry::{ application::{ @@ -72,21 +81,26 @@ impl WebviewRenderer { .with_rpc_handler(move |window: &Window, mut req: RpcRequest| { match req.method.as_str() { "initiate" => { - let mut lock = vdom.write().unwrap(); - let mut reg_lock = registry.write().unwrap(); + let edits = if let Some(edits) = &redits { + serde_json::to_value(edits).unwrap() + } else { + let mut lock = vdom.write().unwrap(); + let mut reg_lock = registry.write().unwrap(); - // Create the thin wrapper around the registry to collect the edits into - let mut real = dom::WebviewDom::new(reg_lock.take().unwrap()); + // Create the thin wrapper around the registry to collect the edits into + let mut real = dom::WebviewDom::new(reg_lock.take().unwrap()); - // Serialize the edit stream - let edits = { - lock.rebuild(&mut real).unwrap(); - serde_json::to_value(&real.edits).unwrap() + // Serialize the edit stream + let edits = { + lock.rebuild(&mut real).unwrap(); + serde_json::to_value(&real.edits).unwrap() + }; + + // Give back the registry into its slot + *reg_lock = Some(real.consume()); + edits }; - // Give back the registry into its slot - *reg_lock = Some(real.consume()); - // Return the edits into the webview runtime Some(RpcResponse::new_result(req.id.take(), Some(edits))) }