From 904b26f7111c3fc66400744ff6192e4b20bf6d74 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 5 Jul 2021 18:37:15 -0400 Subject: [PATCH] feat: add edits back! and more webview support! This commit adds a new type - the DomEdit - for serializing the changes made by the diffing machine. The architecture of how DomEdits fit into the cooperative scheduling is still TBD but it will allow us to build change lists without applying them immediately. This is more performant and allows us to only render parts of the page at a time. This commit also adds more infrastructure around webview. Dioxus can now run on the web, generate static pages, run in the desktop, and run on mobile, with a large part of thanks to webview. --- Cargo.toml | 2 +- README.md | 69 +-- examples/assets/calculator.css | 20 +- packages/cli/src/develop.rs | 5 +- packages/core-macro/src/rsx/element.rs | 8 +- packages/core-macro/src/util.rs | 2 +- packages/core/Cargo.toml | 8 +- packages/core/src/diff.rs | 209 +------- packages/core/src/hooks.rs | 686 +++++++++++++----------- packages/core/src/lib.rs | 3 + packages/core/src/serialize.rs | 52 ++ packages/web/examples/calculator.rs | 112 ++-- packages/web/examples/model.rs | 192 +++++++ packages/webview/ARCHITECTURE.md | 14 + packages/webview/Cargo.toml | 8 +- packages/webview/README.md | 4 +- packages/webview/examples/calculator.rs | 181 +++++++ packages/webview/examples/demo.rs | 94 ++-- packages/webview/examples/hifive.rs | 28 + packages/webview/src/dom.rs | 53 +- packages/webview/src/index.html | 132 ++++- packages/webview/src/lib.rs | 8 +- 22 files changed, 1155 insertions(+), 735 deletions(-) create mode 100644 packages/core/src/serialize.rs create mode 100644 packages/web/examples/model.rs create mode 100644 packages/webview/ARCHITECTURE.md create mode 100644 packages/webview/examples/calculator.rs create mode 100644 packages/webview/examples/hifive.rs diff --git a/Cargo.toml b/Cargo.toml index 917e331de..a84b95285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ members = [ "packages/core", "packages/html-namespace", "packages/web", - # "packages/webview" + "packages/webview", "packages/cli", # "packages/atoms", # "packages/ssr", diff --git a/README.md b/README.md index 2934e2b2c..b8ccc43c4 100644 --- a/README.md +++ b/README.md @@ -104,28 +104,29 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an ### Phase 1: The Basics -| Feature | Dioxus | React | Notes for Dioxus | -| ----------------------- | ------ | ----- | ------------------------------------------------ | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Context | ✅ | ✅ | share state through the tree | -| Hook | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Null components | ✅ | ✅ | allow returning no components | -| No-div components | ✅ | ✅ | components that render components | -| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | -| Suspense | 🛠 | 🛠 | schedule future render from future/promise | -| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | -| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | -| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| Feature | Dioxus | React | Notes for Dioxus | +| ----------------------- | ------ | ----- | ----------------------------------------------------- | +| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | +| Keyed Components | ✅ | ✅ | advanced diffing with keys | +| Web | ✅ | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | ✅ | renderer for desktop | +| Context | ✅ | ✅ | share state through the tree | +| Hook | ✅ | ✅ | memory cells in components | +| SSR | ✅ | ✅ | render directly to string | +| Component Children | ✅ | ✅ | cx.children() as a list of nodes | +| Null components | ✅ | ✅ | allow returning no components | +| No-div components | ✅ | ✅ | components that render components | +| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | +| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | +| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | +| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | +| Suspense | 🛠 | ✅ | schedule future render from future/promise | +| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | +| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | +| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | - [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. - [2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific. @@ -134,23 +135,23 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Feature | Dioxus | React | Notes for Dioxus | | --------------------- | ------ | ----- | ---------------------------------- | -| 1st class router | 👀 | ✅ | Hook built on top of history | -| Assets | 👀 | ✅ | include css/svg/img url statically | -| Integrated classnames | 🛠 | ❓ | built-in `classnames` | -| Transition | 👀 | 🛠 | High-level control over suspense | -| Animation | 👀 | ✅ | Spring-style animations | -| Mobile | 👀 | ✅ | Render with cacao | -| Desktop (native) | 👀 | ✅ | Render with native desktop | -| 3D Renderer | 👀 | ✅ | react-three-fiber | +| 1st class router | 👀 | ✅ | Hook built on top of history | +| Assets | 👀 | ✅ | include css/svg/img url statically | +| Integrated classnames | 🛠 | ❓ | built-in `classnames` | +| Transition | 👀 | 🛠 | High-level control over suspense | +| Animation | 👀 | ✅ | Spring-style animations | +| Mobile | 👀 | ✅ | Render with cacao | +| Desktop (native) | 👀 | ✅ | Render with native desktop | +| 3D Renderer | 👀 | ✅ | react-three-fiber | ### Phase 3: Additional Complexity | Feature | Dioxus | React | Notes for Dioxus | | -------------------- | ------ | ----- | ------------------------------------ | -| Portal | ❓ | ✅ | cast elements through tree | -| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD | -| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | -| LiveView | 👀 | ❓ | Example for SSR + WASM apps | +| Portal | ❓ | ✅ | cast elements through tree | +| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD | +| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | +| LiveView | 👀 | ❓ | Example for SSR + WASM apps | - ✅ = implemented and working - 🛠 = actively being worked on diff --git a/examples/assets/calculator.css b/examples/assets/calculator.css index 808f5e56c..ddada30f8 100644 --- a/examples/assets/calculator.css +++ b/examples/assets/calculator.css @@ -28,7 +28,8 @@ button:active { } #wrapper { - height: 100vh; + /* height: 100vh; */ + height: max-content; display: flex; align-items: center; @@ -58,7 +59,14 @@ button:active { color: white; background: #1c191c; line-height: 130px; - font-size: 6em; + /* font-size: 6em; */ + font-size: 16px; + font-size: 4vw; + + + max-height: 160px; + padding: 0 30px; + /* height: 80px; */ flex: 1; } @@ -68,10 +76,10 @@ button:active { } .calculator-display .auto-scaling-text { - padding: 0 30px; - position: absolute; - right: 0; - transform-origin: right; + /* padding: 0 30px; */ + /* position: absolute; */ + /* right: 0; */ + /* transform-origin: right; */ } .calculator-keypad { diff --git a/packages/cli/src/develop.rs b/packages/cli/src/develop.rs index 5817fc080..1ae016b0d 100644 --- a/packages/cli/src/develop.rs +++ b/packages/cli/src/develop.rs @@ -1,5 +1,5 @@ use crate::{builder::BuildConfig, cli::DevelopOptions, config::Config, error::Result}; -use async_std::{prelude::FutureExt}; +use async_std::prelude::FutureExt; use async_std::future; use async_std::prelude::*; @@ -89,7 +89,8 @@ async fn launch_server(outdir: PathBuf) -> Result<()> { .serve_dir(p)?; let port = "8080"; - let serve_addr = format!("127.0.0.1:{}", port); + let serve_addr = format!("0.0.0.0:{}", port); + // let serve_addr = format!("127.0.0.1:{}", port); info!("App available at http://{}", serve_addr); app.listen(serve_addr).await?; diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index d7180d3a9..74ab04f65 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -161,10 +161,10 @@ impl Parse for ElementAttr { // todo: better error here AttrType::BumpText(s.parse::()?) } - "style" => { - // - todo!("inline style not yet supported") - } + // "style" => { + // // + // todo!("inline style not yet supported") + // } "classes" => { // todo!("custom class lsit not supported") diff --git a/packages/core-macro/src/util.rs b/packages/core-macro/src/util.rs index 84c6391d6..1e595f8f8 100644 --- a/packages/core-macro/src/util.rs +++ b/packages/core-macro/src/util.rs @@ -134,7 +134,7 @@ static HTML_TAGS: Lazy> = Lazy::new(|| { static SVG_TAGS: Lazy> = Lazy::new(|| { [ // SVTG - "svg", "path", "g", + "svg", "path", "g", "text", ] .iter() .cloned() diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 8dcea1814..c4ba2bd2e 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -29,7 +29,7 @@ longest-increasing-subsequence = "0.1.0" log = "0.4" # # Serialize the Edits for use in Webview/Liveview instances -# serde = { version="1", features=["derive"], optional=true } +serde = { version="1", features=["derive"], optional=true } smallvec = "1.6.1" @@ -42,6 +42,6 @@ futures = "0.3.15" [features] -default = [] -serialize = [] -# serialize = ["generational-arena/serde"] +default = ["serialize"] +# default = [] +serialize = ["slotmap/serde", "serde"] diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 9b8574cb8..27524615d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -135,7 +135,7 @@ 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 */ - log::debug!("diffing..."); + // 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". @@ -155,13 +155,11 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // // 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()); self.diff_listeners(old.listeners, new.listeners); @@ -184,7 +182,10 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { let scope = self.components.try_get_mut(scope_id.unwrap()).unwrap(); // .with_scope(scope_id.unwrap(), |scope| { scope.caller = Rc::downgrade(&new.caller); + + // ack - this doesn't work on its own! scope.update_children(new.children); + // }) // .unwrap(); @@ -201,10 +202,10 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { // 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); + // 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); + // log::debug!("scope completed {:#?}", scope_id); self.seen_nodes.insert(scope_id.unwrap()); // }) // .unwrap(); @@ -240,7 +241,6 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { } (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 { @@ -292,200 +292,7 @@ impl<'real, 'bump, Dom: RealDom<'bump>> DiffMachine<'real, 'bump, Dom> { _ => { // - } /* - - */ - // - // 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!(), - // } - // } + } } } @@ -844,7 +651,7 @@ impl<'a, 'bump, Dom: RealDom<'bump>> DiffMachine<'a, 'bump, Dom> { // self.diff_keyed_children(old, new); // self.dom.set_next_temporary(t); } else { - log::debug!("diffing non keyed children"); + // log::debug!("diffing non keyed children"); self.diff_non_keyed_children(old, new); } } diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 3b78405dd..a3785e099 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -5,346 +5,380 @@ //! - [ ] use_reducer //! - [ ] use_effect -pub use new_use_state_def::use_state_new; -pub use use_reducer_def::use_reducer; -pub use use_ref_def::use_ref; -pub use use_state_def::use_state; - use crate::innerlude::Context; -mod use_state_def { - use crate::innerlude::*; - use std::{ - cell::RefCell, - ops::{Deref, DerefMut}, - rc::Rc, - }; +use crate::innerlude::*; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, +}; - /// Store state between component renders! - /// When called, this hook retrives a stored value and provides a setter to update that value. - /// When the setter is called, the component is re-ran with the new value. - /// - /// This is behaves almost exactly the same way as React's "use_state". - /// - /// Usage: - /// ```ignore - /// static Example: FC<()> = |cx| { - /// let (counter, set_counter) = use_state(&cx, || 0); - /// let increment = |_| set_couter(counter + 1); - /// let decrement = |_| set_couter(counter + 1); - /// - /// html! { - ///
- ///

"Counter: {counter}"

- /// - /// - ///
- /// } - /// } - /// ``` - pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>( - cx: &impl Scoped<'a>, - initial_state_fn: F, - ) -> (&'a T, &'a Rc) { - struct UseState { - new_val: Rc>>, - current_val: T, - caller: Rc, - } - - cx.use_hook( - move || UseState { - new_val: Rc::new(RefCell::new(None)), - current_val: initial_state_fn(), - caller: Rc::new(|_| println!("setter called!")), - }, - move |hook| { - log::debug!("Use_state set called"); - let inner = hook.new_val.clone(); - let scheduled_update = cx.schedule_update(); - - // get ownership of the new val and replace the current with the new - // -> as_ref -> borrow_mut -> deref_mut -> take - // -> rc -> &RefCell -> RefMut -> &Option -> T - if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { - hook.current_val = new_val; - } - - // todo: swap out the caller with a subscription call and an internal update - hook.caller = Rc::new(move |new_val| { - // update the setter with the new value - let mut new_inner = inner.as_ref().borrow_mut(); - *new_inner = Some(new_val); - - // Ensure the component gets updated - scheduled_update(); - }); - - // box gets derefed into a ref which is then taken as ref with the hook - (&hook.current_val, &hook.caller) - }, - |_| {}, - ) - } -} - -mod new_use_state_def { - use crate::innerlude::*; - use std::{ - cell::RefCell, - fmt::{Debug, Display}, - ops::{Deref, DerefMut}, - pin::Pin, - rc::Rc, - }; - - pub struct UseState { - modifier: Rc>>>, - current_val: T, - update: Box, - setter: Box, - // setter: Box, - } - - // #[derive(Clone, Copy)] - // pub struct UseStateHandle<'a, T: 'static> { - // inner: &'a UseState, - // // pub setter: &'a dyn Fn(T), - // // pub modifier: &'a dyn Fn(&mut T), - // } - - impl<'a, T: 'static> UseState { - pub fn setter(&self) -> &dyn Fn(T) { - &self.setter - // let r = self.setter.as_mut(); - // unsafe { Pin::get_unchecked_mut(r) } - } - - pub fn set(&self, new_val: T) { - self.modify(|f| *f = new_val); - } - - // signal that we need to be updated - // save the modifier - pub fn modify(&self, f: impl FnOnce(&mut T) + 'static) { - let mut slot = self.modifier.as_ref().borrow_mut(); - *slot = Some(Box::new(f)); - (self.update)(); - } - } - impl<'a, T: 'static> std::ops::Deref for UseState { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.current_val - } - } - - // enable displaty for the handle - impl<'a, T: 'static + Display> std::fmt::Display for UseState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } - } - - /// Store state between component renders! - /// When called, this hook retrives a stored value and provides a setter to update that value. - /// When the setter is called, the component is re-ran with the new value. - /// - /// This is behaves almost exactly the same way as React's "use_state". - /// - /// Usage: - /// ```ignore - /// static Example: FC<()> = |cx| { - /// let (counter, set_counter) = use_state(&cx, || 0); - /// let increment = |_| set_couter(counter + 1); - /// let decrement = |_| set_couter(counter + 1); - /// - /// html! { - ///
- ///

"Counter: {counter}"

- /// - /// - ///
- /// } - /// } - /// ``` - pub fn use_state_new<'a, 'c, T: 'static, F: FnOnce() -> T>( - cx: &impl Scoped<'a>, - initial_state_fn: F, - ) -> &'a UseState { - cx.use_hook( - move || UseState { - modifier: Rc::new(RefCell::new(None)), - current_val: initial_state_fn(), - update: Box::new(|| {}), - setter: Box::new(|_| {}), - }, - move |hook| { - log::debug!("addr of hook: {:#?}", hook as *const _); - let scheduled_update = cx.schedule_update(); - - // log::debug!("Checking if new value {:#?}", &hook.current_val); - // get ownership of the new val and replace the current with the new - // -> as_ref -> borrow_mut -> deref_mut -> take - // -> rc -> &RefCell -> RefMut -> &Option -> T - if let Some(new_val) = hook.modifier.as_ref().borrow_mut().deref_mut().take() { - // log::debug!("setting prev {:#?}", &hook.current_val); - (new_val)(&mut hook.current_val); - // log::debug!("setting new value {:#?}", &hook.current_val); - } - - hook.update = Box::new(move || scheduled_update()); - - let modifier = hook.modifier.clone(); - hook.setter = Box::new(move |new_val: T| { - let mut slot = modifier.as_ref().borrow_mut(); - - let slot2 = slot.deref_mut(); - *slot2 = Some(Box::new(move |old: &mut T| *old = new_val)); - }); - - &*hook - }, - |_| {}, - ) - } -} - -mod use_ref_def { - use crate::innerlude::*; - use std::{cell::RefCell, ops::DerefMut}; - - // pub struct UseRef { - // _current: RefCell, - // } - // impl UseRef { - // fn new(val: T) -> Self { - // Self { - // _current: RefCell::new(val), - // } - // } - - // pub fn set(&self, new: T) { - // let mut val = self._current.borrow_mut(); - // *val = new; - // } - - // pub fn modify(&self, modifier: impl FnOnce(&mut T)) { - // let mut val = self._current.borrow_mut(); - // let val_as_ref = val.deref_mut(); - // modifier(val_as_ref); - // } - - // pub fn current(&self) -> std::cell::Ref<'_, T> { - // self._current.borrow() - // } - // } - - /// Store a mutable value between renders! - /// To read the value, borrow the ref. - /// To change it, use modify. - /// Modifications to this value do not cause updates to the component - /// Attach to inner context reference, so context can be consumed - pub fn use_ref<'a, T: 'static>( - cx: &impl Scoped<'a>, - initial_state_fn: impl FnOnce() -> T + 'static, - ) -> &'a RefCell { - cx.use_hook(|| RefCell::new(initial_state_fn()), |state| &*state, |_| {}) - } -} - -mod use_reducer_def { - use crate::innerlude::*; - use std::{cell::RefCell, ops::DerefMut, rc::Rc}; - - struct UseReducer { +/// Store state between component renders! +/// When called, this hook retrives a stored value and provides a setter to update that value. +/// When the setter is called, the component is re-ran with the new value. +/// +/// This is behaves almost exactly the same way as React's "use_state". +/// +/// Usage: +/// ```ignore +/// static Example: FC<()> = |cx| { +/// let (counter, set_counter) = use_state(&cx, || 0); +/// let increment = |_| set_couter(counter + 1); +/// let decrement = |_| set_couter(counter + 1); +/// +/// html! { +///
+///

"Counter: {counter}"

+/// +/// +///
+/// } +/// } +/// ``` +pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>( + cx: &impl Scoped<'a>, + initial_state_fn: F, +) -> (&'a T, &'a Rc) { + struct UseState { new_val: Rc>>, current_val: T, - caller: Box, + caller: Rc, } - /// Store state between component renders! - /// When called, this hook retrives a stored value and provides a setter to update that value. - /// When the setter is called, the component is re-ran with the new value. - /// - /// This is behaves almost exactly the same way as React's "use_state". - /// - pub fn use_reducer<'a, 'c, State: 'static, Action: 'static>( - cx: &impl Scoped<'a>, - initial_state_fn: impl FnOnce() -> State, - _reducer: impl Fn(&mut State, Action), - ) -> (&'a State, &'a Box) { - cx.use_hook( - move || UseReducer { - new_val: Rc::new(RefCell::new(None)), - current_val: initial_state_fn(), - caller: Box::new(|_| println!("setter called!")), - }, - move |hook| { - let _inner = hook.new_val.clone(); - let scheduled_update = cx.schedule_update(); + cx.use_hook( + move || UseState { + new_val: Rc::new(RefCell::new(None)), + current_val: initial_state_fn(), + caller: Rc::new(|_| println!("setter called!")), + }, + move |hook| { + log::debug!("Use_state set called"); + let inner = hook.new_val.clone(); + let scheduled_update = cx.schedule_update(); - // get ownership of the new val and replace the current with the new - // -> as_ref -> borrow_mut -> deref_mut -> take - // -> rc -> &RefCell -> RefMut -> &Option -> T - if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { - hook.current_val = new_val; - } + // get ownership of the new val and replace the current with the new + // -> as_ref -> borrow_mut -> deref_mut -> take + // -> rc -> &RefCell -> RefMut -> &Option -> T + if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { + hook.current_val = new_val; + } - // todo: swap out the caller with a subscription call and an internal update - hook.caller = Box::new(move |_new_val| { - // update the setter with the new value - // let mut new_inner = inner.as_ref().borrow_mut(); - // *new_inner = Some(new_val); + // todo: swap out the caller with a subscription call and an internal update + hook.caller = Rc::new(move |new_val| { + // update the setter with the new value + let mut new_inner = inner.as_ref().borrow_mut(); + *new_inner = Some(new_val); - // Ensure the component gets updated - scheduled_update(); - }); + // Ensure the component gets updated + scheduled_update(); + }); - // box gets derefed into a ref which is then taken as ref with the hook - (&hook.current_val, &hook.caller) - }, - |_| {}, - ) + // box gets derefed into a ref which is then taken as ref with the hook + (&hook.current_val, &hook.caller) + }, + |_| {}, + ) +} + +use crate::innerlude::*; +use std::{ + fmt::{Debug, Display}, + pin::Pin, +}; + +pub struct UseState { + modifier: Rc>>>, + current_val: T, + update: Box, + setter: Box, + // setter: Box, +} + +// #[derive(Clone, Copy)] +// pub struct UseStateHandle<'a, T: 'static> { +// inner: &'a UseState, +// // pub setter: &'a dyn Fn(T), +// // pub modifier: &'a dyn Fn(&mut T), +// } + +impl<'a, T: 'static> UseState { + pub fn setter(&self) -> &dyn Fn(T) { + &self.setter + // let r = self.setter.as_mut(); + // unsafe { Pin::get_unchecked_mut(r) } } - // #[cfg(test)] - mod tests { - - use crate::prelude::*; - - enum Actions { - Incr, - Decr, - } - - // #[allow(unused)] - // static Example: FC<()> = |cx| { - // let (count, reduce) = use_reducer( - // &cx, - // || 0, - // |count, action| match action { - // Actions::Incr => *count += 1, - // Actions::Decr => *count -= 1, - // }, - // ); - - // cx.render(rsx! { - // div { - // h1 {"Count: {count}"} - // button { - // "Increment" - // onclick: move |_| reduce(Actions::Incr) - // } - // button { - // "Decrement" - // onclick: move |_| reduce(Actions::Decr) - // } - // } - // }) - // }; + pub fn set(&self, new_val: T) { + self.modify(|f| *f = new_val); } + + // signal that we need to be updated + // save the modifier + pub fn modify(&self, f: impl FnOnce(&mut T) + 'static) { + let mut slot = self.modifier.as_ref().borrow_mut(); + *slot = Some(Box::new(f)); + (self.update)(); + } +} +impl<'a, T: 'static> std::ops::Deref for UseState { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.current_val + } +} + +// enable displaty for the handle +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +/// Store state between component renders! +/// When called, this hook retrives a stored value and provides a setter to update that value. +/// When the setter is called, the component is re-ran with the new value. +/// +/// This is behaves almost exactly the same way as React's "use_state". +/// +/// Usage: +/// ```ignore +/// static Example: FC<()> = |cx| { +/// let (counter, set_counter) = use_state(&cx, || 0); +/// let increment = |_| set_couter(counter + 1); +/// let decrement = |_| set_couter(counter + 1); +/// +/// html! { +///
+///

"Counter: {counter}"

+/// +/// +///
+/// } +/// } +/// ``` +pub fn use_state_new<'a, 'c, T: 'static, F: FnOnce() -> T>( + cx: &impl Scoped<'a>, + initial_state_fn: F, +) -> &'a UseState { + cx.use_hook( + move || UseState { + modifier: Rc::new(RefCell::new(None)), + current_val: initial_state_fn(), + update: Box::new(|| {}), + setter: Box::new(|_| {}), + }, + move |hook| { + log::debug!("addr of hook: {:#?}", hook as *const _); + let scheduled_update = cx.schedule_update(); + + // log::debug!("Checking if new value {:#?}", &hook.current_val); + // get ownership of the new val and replace the current with the new + // -> as_ref -> borrow_mut -> deref_mut -> take + // -> rc -> &RefCell -> RefMut -> &Option -> T + if let Some(new_val) = hook.modifier.as_ref().borrow_mut().deref_mut().take() { + // log::debug!("setting prev {:#?}", &hook.current_val); + (new_val)(&mut hook.current_val); + // log::debug!("setting new value {:#?}", &hook.current_val); + } + + hook.update = Box::new(move || scheduled_update()); + + let modifier = hook.modifier.clone(); + hook.setter = Box::new(move |new_val: T| { + let mut slot = modifier.as_ref().borrow_mut(); + + let slot2 = slot.deref_mut(); + *slot2 = Some(Box::new(move |old: &mut T| *old = new_val)); + }); + + &*hook + }, + |_| {}, + ) +} + +// pub struct UseRef { +// _current: RefCell, +// } +// impl UseRef { +// fn new(val: T) -> Self { +// Self { +// _current: RefCell::new(val), +// } +// } + +// pub fn set(&self, new: T) { +// let mut val = self._current.borrow_mut(); +// *val = new; +// } + +// pub fn modify(&self, modifier: impl FnOnce(&mut T)) { +// let mut val = self._current.borrow_mut(); +// let val_as_ref = val.deref_mut(); +// modifier(val_as_ref); +// } + +// pub fn current(&self) -> std::cell::Ref<'_, T> { +// self._current.borrow() +// } +// } + +/// Store a mutable value between renders! +/// To read the value, borrow the ref. +/// To change it, use modify. +/// Modifications to this value do not cause updates to the component +/// Attach to inner context reference, so context can be consumed +pub fn use_ref<'a, T: 'static>( + cx: &impl Scoped<'a>, + initial_state_fn: impl FnOnce() -> T + 'static, +) -> &'a RefCell { + cx.use_hook(|| RefCell::new(initial_state_fn()), |state| &*state, |_| {}) +} + +struct UseReducer { + new_val: Rc>>, + current_val: T, + caller: Box, +} + +/// Store state between component renders! +/// When called, this hook retrives a stored value and provides a setter to update that value. +/// When the setter is called, the component is re-ran with the new value. +/// +/// This is behaves almost exactly the same way as React's "use_state". +/// +pub fn use_reducer<'a, 'c, State: 'static, Action: 'static>( + cx: &impl Scoped<'a>, + initial_state_fn: impl FnOnce() -> State, + _reducer: impl Fn(&mut State, Action), +) -> (&'a State, &'a Box) { + cx.use_hook( + move || UseReducer { + new_val: Rc::new(RefCell::new(None)), + current_val: initial_state_fn(), + caller: Box::new(|_| println!("setter called!")), + }, + move |hook| { + let _inner = hook.new_val.clone(); + let scheduled_update = cx.schedule_update(); + + // get ownership of the new val and replace the current with the new + // -> as_ref -> borrow_mut -> deref_mut -> take + // -> rc -> &RefCell -> RefMut -> &Option -> T + if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { + hook.current_val = new_val; + } + + // todo: swap out the caller with a subscription call and an internal update + hook.caller = Box::new(move |_new_val| { + // update the setter with the new value + // let mut new_inner = inner.as_ref().borrow_mut(); + // *new_inner = Some(new_val); + + // Ensure the component gets updated + scheduled_update(); + }); + + // box gets derefed into a ref which is then taken as ref with the hook + (&hook.current_val, &hook.caller) + }, + |_| {}, + ) +} + +/// 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 +pub fn use_model<'a, T: ToOwned + 'static>( + cx: &impl Scoped<'a>, + f: impl FnOnce() -> T, +) -> &'a UseModel { + cx.use_hook( + move || { + let real = f(); + let wip = RefCell::new(real.to_owned()); + let update = cx.schedule_update(); + UseModel { real, wip, update } + }, + |hook| { + hook.real = hook.wip.borrow().to_owned(); + &*hook + }, + |_| {}, + ) +} + +pub struct UseModel { + real: T, + wip: RefCell, + update: Rc, +} + +use std::cell::{Ref, RefMut}; +impl Deref for UseModel { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.real + } +} + +impl Display for UseModel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.real) + } +} + +impl UseModel { + pub fn get_mut(&self) -> RefMut<'_, T> { + (self.update)(); + self.wip.borrow_mut() + } + pub fn modify(&self, f: impl FnOnce(&mut T)) { + (self.update)(); + let mut g = self.get_mut(); + let r = g.deref_mut(); + f(r) + } +} + +// #[cfg(test)] +mod tests { + + use crate::prelude::*; + + enum Actions { + Incr, + Decr, + } + + // #[allow(unused)] + // static Example: FC<()> = |cx| { + // let (count, reduce) = use_reducer( + // &cx, + // || 0, + // |count, action| match action { + // Actions::Incr => *count += 1, + // Actions::Decr => *count -= 1, + // }, + // ); + + // cx.render(rsx! { + // div { + // h1 {"Count: {count}"} + // button { + // "Increment" + // onclick: move |_| reduce(Actions::Incr) + // } + // button { + // "Decrement" + // onclick: move |_| reduce(Actions::Decr) + // } + // } + // }) + // }; } pub fn use_is_initialized

(cx: Context

) -> bool { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index e6bf59533..359d306c4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -8,6 +8,9 @@ //! //! +#[cfg(feature = "serialize")] +pub mod serialize; + pub mod arena; pub mod component; pub mod util; // Logic for extending FC diff --git a/packages/core/src/serialize.rs b/packages/core/src/serialize.rs new file mode 100644 index 000000000..b04e321fa --- /dev/null +++ b/packages/core/src/serialize.rs @@ -0,0 +1,52 @@ +use crate::prelude::ScopeIdx; +use serde::{Deserialize, Serialize}; + +/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the +/// network or through FFI boundaries. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum DomEdits<'bump> { + PushRoot { + root: u64, + }, + AppendChild, + ReplaceWith, + Remove, + RemoveAllChildren, + CreateTextNode { + text: &'bump str, + id: u64, + }, + CreateElement { + tag: &'bump str, + id: u64, + }, + CreateElementNs { + tag: &'bump str, + id: u64, + ns: &'bump str, + }, + CreatePlaceholder { + id: u64, + }, + NewEventListener { + event: &'bump str, + scope: ScopeIdx, + node: u64, + idx: usize, + }, + RemoveEventListener { + event: &'bump str, + }, + SetText { + text: &'bump str, + }, + SetAttribute { + field: &'bump str, + value: &'bump str, + ns: Option<&'bump str>, + }, + RemoveAttribute { + name: &'bump str, + }, +} diff --git a/packages/web/examples/calculator.rs b/packages/web/examples/calculator.rs index bf1b8d554..c17bdc755 100644 --- a/packages/web/examples/calculator.rs +++ b/packages/web/examples/calculator.rs @@ -1,26 +1,24 @@ //! Example: Calculator -//! ------------------------- - -// use dioxus::events::on::*; -// use dioxus::prelude::*; +//! ------------------ +//! +//! This example showcases a basic iOS-style calculator app using classic react-style `use_state` hooks. A counterpart of +//! this example is in `model.rs` which shows the same calculator app implemented using the `model` paradigm. We've found +//! that the most natural Rust code is more "model-y" than "React-y", but opt to keep this example to show the different +//! flavors of programming you can use in Dioxus. +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, @@ -31,18 +29,27 @@ enum Operator { 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 (display_value, set_display_value) = use_state(&cx, || "0".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_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(); @@ -84,14 +91,12 @@ static App: FC<()> = |cx| { 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), @@ -110,33 +115,34 @@ static App: FC<()> = |cx| { }; 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() "=" } + div { + id: "wrapper" + div { class: "app" onkeydown: {keydownhandler} + style { "{STYLE}" } + div { class: "calculator", + CalculatorDisplay { val: &display_value} + 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} "%"} + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "●" } + {(1..10).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() "=" } + } } } } @@ -153,7 +159,7 @@ struct CalculatorKeyProps<'a> { onclick: &'a dyn Fn(MouseEvent), } -fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { +fn CalculatorKey<'a>(cx: Context<'a, CalculatorKeyProps>) -> VNode<'a> { cx.render(rsx! { button { class: "calculator-key {cx.name}" @@ -164,18 +170,18 @@ fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { } #[derive(Props, PartialEq)] -struct CalculatorDisplayProps { - val: f64, +struct CalculatorDisplayProps<'a> { + val: &'a str, } -fn CalculatorDisplay(cx: Context) -> VNode { +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.separated_string(); + let formatted = cx.val.parse::().unwrap().separated_string(); // TODO: make it autoscaling with css cx.render(rsx! { div { class: "calculator-display" - "{formatted}" + div { class: "auto-scaling-text", "{display_value}" } } }) } diff --git a/packages/web/examples/model.rs b/packages/web/examples/model.rs new file mode 100644 index 000000000..e233b432e --- /dev/null +++ b/packages/web/examples/model.rs @@ -0,0 +1,192 @@ +//! Example: Calculator +//! ------------------- +//! +//! 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 "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 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)); +} + +static App: FC<()> = |cx| { + let state = use_model(&cx, || Calculator::new()); + + let clear_display = state.display_value.eq("0"); + let clear_text = if clear_display { "C" } else { "AC" }; + let formatted = state.formatted_display(); + + cx.render(rsx! { + 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: 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 |_| 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 |_| state.get_mut().input_digit(k), "{k}" } + })} + } + } + div { class: "operator-keys" + 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(), "=" } + } + } + } + } + } + }) +}; + +#[derive(Props)] +struct CalculatorKeyProps<'a> { + name: &'static str, + 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(Clone)] +struct Calculator { + display_value: String, + operator: Option, + waiting_for_operand: bool, + cur_val: f64, +} +#[derive(Clone)] +enum Operator { + Add, + Sub, + Mul, + Div, +} + +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/packages/webview/ARCHITECTURE.md b/packages/webview/ARCHITECTURE.md new file mode 100644 index 000000000..5a937dbca --- /dev/null +++ b/packages/webview/ARCHITECTURE.md @@ -0,0 +1,14 @@ +# dioxus webview arch + + +dioxus webview represents a usecase of dioxus to stream edits from one dom to another. In this particular case, we stream edits from a native process into the webview's runtime. WebView is really portable, so we can leverage the UI of iOS, Android, and all desktop platforms while also maintaining direct access to platform-specific APIs. It's the best of both worlds! + + +For this to work properly, we need to ship a universal client that receives Dioxus edits from a remote process. To make our lives (as developers) a bit easier, the client code is currently implemented in JS (subject to change!). + +The provider (the thing that talks to the client on the other side of the network) is still written in Rust, and is supplied through the dioxus-webview crate. The client is much more "dumb" than the provider - the provider must handle NodeID generation, flushing changes, priorities, managing events, and more. + + +## update; wry! + +Wry is an effort by the Tauri team to make fully native diff --git a/packages/webview/Cargo.toml b/packages/webview/Cargo.toml index 1ef708be9..edf541971 100644 --- a/packages/webview/Cargo.toml +++ b/packages/webview/Cargo.toml @@ -11,15 +11,15 @@ license = "MIT/Apache-2.0" [dependencies] # web-view = { git = "https://github.com/Boscop/web-view" } web-view = "0.7.3" -dioxus-core = { path = "../core", version = "0.1.2", features = ["serialize"] } +dioxus-core = { path="../core", version="0.1.2", features=["serialize"] } anyhow = "1.0.38" argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" -async-std = { version = "1.9.0", features = ["attributes"] } +async-std = { version="1.9.0", features=["attributes"] } thiserror = "1.0.23" log = "0.4.13" -fern = { version = "0.6.0", features = ["colored"] } +fern = { version="0.6.0", features=["colored"] } # thiserror = "1.0.23" # log = "0.4.13" @@ -32,6 +32,8 @@ fern = { version = "0.6.0", features = ["colored"] } # async-std = { version = "1.9.0", features = ["attributes"] } tide = "0.15.0" tide-websockets = "0.3.0" +html-escape = "0.2.9" +# wry = "0.10.3" [build-dependencies] diff --git a/packages/webview/README.md b/packages/webview/README.md index d9ecd0464..5c2d66560 100644 --- a/packages/webview/README.md +++ b/packages/webview/README.md @@ -10,7 +10,7 @@ Dioxus-webview is an attempt at making a simpler "Tauri" where creating desktop async fn main() { dioxus_webview::new(|cx| { let (count, set_count) = use_state(&cx, || 0); - html! { + cx.render(html! {

"Dioxus Desktop Demo"

"Count is {count}"

@@ -18,7 +18,7 @@ async fn main() { "Click to increment"
- } + }) }) .configure_webview(|view| { // custom webview config options diff --git a/packages/webview/examples/calculator.rs b/packages/webview/examples/calculator.rs new file mode 100644 index 000000000..f7bbe9855 --- /dev/null +++ b/packages/webview/examples/calculator.rs @@ -0,0 +1,181 @@ +use dioxus::events::on::*; +use dioxus::prelude::*; +use dioxus_core as dioxus; + +const STYLE: &str = include_str!("../../../examples/assets/calculator.css"); + +fn main() { + dioxus_webview::launch( + |builder| { + builder + .title("Test Dioxus App") + .size(340, 560) + .resizable(false) + .debug(true) + }, + (), + App, + ) + .expect("Webview finished"); +} + +static App: FC<()> = |cx| { + let state = use_model(&cx, || Calculator::new()); + + let clear_display = state.display_value.eq("0"); + let clear_text = if clear_display { "C" } else { "AC" }; + let formatted = state.formatted_display(); + + cx.render(rsx! { + 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: 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 |_| 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 |_| state.get_mut().input_digit(k), "{k}" } + })} + } + } + div { class: "operator-keys" + 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(), "=" } + } + } + } + } + } + }) +}; + +#[derive(Props)] +struct CalculatorKeyProps<'a> { + name: &'static str, + 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(Clone)] +struct Calculator { + display_value: String, + operator: Option, + waiting_for_operand: bool, + cur_val: f64, +} +#[derive(Clone)] +enum Operator { + Add, + Sub, + Mul, + Div, +} + +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.clone() + // .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/packages/webview/examples/demo.rs b/packages/webview/examples/demo.rs index 51604478b..0d5d9e540 100644 --- a/packages/webview/examples/demo.rs +++ b/packages/webview/examples/demo.rs @@ -1,7 +1,7 @@ //! An example where the dioxus vdom is running in a native thread, interacting with webview //! Content is passed from the native thread into the webview +use dioxus_core as dioxus; use dioxus_core::prelude::*; - fn main() { dioxus_webview::launch( |builder| { @@ -17,49 +17,55 @@ fn main() { .expect("Webview finished"); } -static Example: FC<()> = |cx| { - cx.render(html! { -
- - - - - - -
- }) -}; // static Example: FC<()> = |cx| { -// cx.render(rsx! { -// div { -// class: "flex items-center justify-center flex-col" -// div { -// class: "flex items-center justify-center" -// div { -// class: "flex flex-col bg-white rounded p-4 w-full max-w-xs" -// div { class: "font-bold text-xl", "Example desktop app" } -// div { class: "text-sm text-gray-500", "This is running natively" } -// div { -// class: "flex flex-row items-center justify-center mt-6" -// div { class: "font-medium text-6xl", "100%" } -// } -// div { -// class: "flex flex-row justify-between mt-6" -// a { -// href: "https://www.dioxuslabs.com" -// class: "underline" -// "Made with dioxus" -// } -// } -// } -// } -// } +// cx.render(html! { +//
+// + +// +// + +// +//
// }) // }; +static Example: FC<()> = |cx| { + cx.render(rsx! { + div { + class: "flex items-center justify-center flex-col" + div { + class: "flex items-center justify-center" + div { + class: "flex flex-col bg-white rounded p-4 w-full max-w-xs" + div { class: "font-bold text-xl", "Example desktop app" } + div { class: "text-sm text-gray-500", "This is running natively" } + div { + class: "flex flex-row items-center justify-center mt-6" + div { class: "font-medium text-6xl", "100%" } + } + div { + class: "flex flex-row justify-between mt-6" + a { + href: "https://www.dioxuslabs.com" + class: "underline" + "Made with dioxus" + } + } + ul { + {(0..10).map(|f| rsx!(li { + key: "{f}" + "{f}" + }))} + } + } + } + } + }) +}; diff --git a/packages/webview/examples/hifive.rs b/packages/webview/examples/hifive.rs new file mode 100644 index 000000000..24f5b9d02 --- /dev/null +++ b/packages/webview/examples/hifive.rs @@ -0,0 +1,28 @@ +//! An example where the dioxus vdom is running in a native thread, interacting with webview +//! Content is passed from the native thread into the webview +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +fn main() { + dioxus_webview::launch( + |builder| { + builder + .title("Test Dioxus App") + .size(320, 480) + .resizable(false) + .debug(true) + }, + (), + App, + ) + .expect("Webview finished"); +} + +static App: FC<()> = |cx| { + let hifives = use_model(&cx, || 0); + cx.render(rsx! { + div { + h1 { "Hi-fives collected: {hifives}" } + button { "Hi five me!", onclick: move |_| *hifives.get_mut() += 1 } + } + }) +}; diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index 2cc4ab103..b6d792972 100644 --- a/packages/webview/src/dom.rs +++ b/packages/webview/src/dom.rs @@ -2,12 +2,14 @@ use dioxus_core as dioxus; use dioxus_core::prelude::*; +use dioxus_core::serialize::DomEdits; use dioxus_core::{ diff::RealDom, prelude::ScopeIdx, virtual_dom::{RealDomNode, VirtualDom}, }; use serde::{Deserialize, Serialize}; +use DomEdits::*; fn test() { const App: FC<()> = |cx| cx.render(rsx! { div {}}); @@ -20,7 +22,7 @@ fn test() { } pub struct WebviewDom<'bump> { - pub edits: Vec>, + pub edits: Vec>, pub node_counter: u64, } impl WebviewDom<'_> { @@ -31,55 +33,6 @@ impl WebviewDom<'_> { } } } - -#[derive(Debug, Serialize, Deserialize)] -pub enum SerializedDom<'bump> { - PushRoot { - root: u64, - }, - AppendChild, - ReplaceWith, - Remove, - RemoveAllChildren, - CreateTextNode { - text: &'bump str, - id: u64, - }, - CreateElement { - tag: &'bump str, - id: u64, - }, - CreateElementNs { - tag: &'bump str, - id: u64, - ns: &'bump str, - }, - CreatePlaceholder { - id: u64, - }, - NewEventListener { - event: &'bump str, - scope: ScopeIdx, - node: u64, - idx: usize, - }, - RemoveEventListener { - event: &'bump str, - }, - SetText { - text: &'bump str, - }, - SetAttribute { - field: &'bump str, - value: &'bump str, - ns: Option<&'bump str>, - }, - RemoveAttribute { - name: &'bump str, - }, -} - -use SerializedDom::*; impl<'bump> RealDom<'bump> for WebviewDom<'bump> { fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) { self.edits.push(PushRoot { root: root.0 }); diff --git a/packages/webview/src/index.html b/packages/webview/src/index.html index 9beef451b..01a7e9982 100644 --- a/packages/webview/src/index.html +++ b/packages/webview/src/index.html @@ -1 +1,131 @@ - + + + + + + + + + + + +
+ + + + diff --git a/packages/webview/src/lib.rs b/packages/webview/src/lib.rs index 71b090a32..98073c4b1 100644 --- a/packages/webview/src/lib.rs +++ b/packages/webview/src/lib.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use dioxus_core::prelude::*; use dioxus_core::virtual_dom::VirtualDom; -use web_view::Handle; +use web_view::{escape, Handle}; use web_view::{WVResult, WebView, WebViewBuilder}; mod dom; -static HTML_CONTENT: &'static str = include_str!("./../../liveview/index.html"); +static HTML_CONTENT: &'static str = include_str!("./index.html"); pub fn launch( builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder, @@ -79,13 +79,15 @@ impl WebviewRenderer { view.step() .expect("should not fail") .expect("should not fail"); + std::thread::sleep(std::time::Duration::from_millis(15)); if let Ok(event) = receiver.try_recv() { if let InnerEvent::Initiate(handle) = event { let editlist = ref_edits.clone(); handle .dispatch(move |view| { - view.eval(format!("EditListReceived(`{}`);", editlist).as_str()) + let escaped = escape(&editlist); + view.eval(&format!("EditListReceived({});", escaped)) }) .expect("Dispatch failed"); }