Sfoglia il codice sorgente

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.
Jonathan Kelley 4 anni fa
parent
commit
904b26f

+ 1 - 1
Cargo.toml

@@ -59,7 +59,7 @@ members = [
     "packages/core",
     "packages/html-namespace",
     "packages/web",
-    # "packages/webview"
+    "packages/webview",
     "packages/cli",
     # "packages/atoms",
     # "packages/ssr",

+ 35 - 34
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

+ 14 - 6
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 {

+ 3 - 2
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?;

+ 4 - 4
packages/core-macro/src/rsx/element.rs

@@ -161,10 +161,10 @@ impl Parse for ElementAttr {
                     // todo: better error here
                     AttrType::BumpText(s.parse::<LitStr>()?)
                 }
-                "style" => {
-                    //
-                    todo!("inline style not yet supported")
-                }
+                // "style" => {
+                //     //
+                //     todo!("inline style not yet supported")
+                // }
                 "classes" => {
                     //
                     todo!("custom class lsit not supported")

+ 1 - 1
packages/core-macro/src/util.rs

@@ -134,7 +134,7 @@ static HTML_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
 static SVG_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
     [
         // SVTG
-        "svg", "path", "g",
+        "svg", "path", "g", "text",
     ]
     .iter()
     .cloned()

+ 4 - 4
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"]

+ 8 - 201
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);
         }
     }

+ 346 - 312
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,
-    };
-
-    /// 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! {
-    ///         <div>
-    ///             <h1>"Counter: {counter}" </h1>
-    ///             <button onclick={increment}> "Increment" </button>
-    ///             <button onclick={decrement}> "Decrement" </button>
-    ///         </div>  
-    ///     }
-    /// }
-    /// ```
-    pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>(
-        cx: &impl Scoped<'a>,
-        initial_state_fn: F,
-    ) -> (&'a T, &'a Rc<dyn Fn(T)>) {
-        struct UseState<T: 'static> {
-            new_val: Rc<RefCell<Option<T>>>,
-            current_val: T,
-            caller: Rc<dyn Fn(T) + 'static>,
-        }
-
-        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> -> 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<T: 'static> {
-        modifier: Rc<RefCell<Option<Box<dyn FnOnce(&mut T)>>>>,
+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! {
+///         <div>
+///             <h1>"Counter: {counter}" </h1>
+///             <button onclick={increment}> "Increment" </button>
+///             <button onclick={decrement}> "Decrement" </button>
+///         </div>  
+///     }
+/// }
+/// ```
+pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>(
+    cx: &impl Scoped<'a>,
+    initial_state_fn: F,
+) -> (&'a T, &'a Rc<dyn Fn(T)>) {
+    struct UseState<T: 'static> {
+        new_val: Rc<RefCell<Option<T>>>,
         current_val: T,
-        update: Box<dyn Fn() + 'static>,
-        setter: Box<dyn Fn(T) + 'static>,
-        // setter: Box<dyn Fn(T) + 'static>,
+        caller: Rc<dyn Fn(T) + 'static>,
     }
 
-    // #[derive(Clone, Copy)]
-    // pub struct UseStateHandle<'a, T: 'static> {
-    //     inner: &'a UseState<T>,
-    //     // pub setter: &'a dyn Fn(T),
-    //     // pub modifier: &'a dyn Fn(&mut T),
-    // }
-
-    impl<'a, T: 'static> UseState<T> {
-        pub fn setter(&self) -> &dyn Fn(T) {
-            &self.setter
-            // let r = self.setter.as_mut();
-            // unsafe { Pin::get_unchecked_mut(r) }
-        }
+    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> -> 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)
+        },
+        |_| {},
+    )
+}
 
-        pub fn set(&self, new_val: T) {
-            self.modify(|f| *f = new_val);
-        }
+use crate::innerlude::*;
+use std::{
+    fmt::{Debug, Display},
+    pin::Pin,
+};
+
+pub struct UseState<T: 'static> {
+    modifier: Rc<RefCell<Option<Box<dyn FnOnce(&mut T)>>>>,
+    current_val: T,
+    update: Box<dyn Fn() + 'static>,
+    setter: Box<dyn Fn(T) + 'static>,
+    // setter: Box<dyn Fn(T) + 'static>,
+}
 
-        // 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)();
-        }
+// #[derive(Clone, Copy)]
+// pub struct UseStateHandle<'a, T: 'static> {
+//     inner: &'a UseState<T>,
+//     // pub setter: &'a dyn Fn(T),
+//     // pub modifier: &'a dyn Fn(&mut T),
+// }
+
+impl<'a, T: 'static> UseState<T> {
+    pub fn setter(&self) -> &dyn Fn(T) {
+        &self.setter
+        // let r = self.setter.as_mut();
+        // unsafe { Pin::get_unchecked_mut(r) }
     }
-    impl<'a, T: 'static> std::ops::Deref for UseState<T> {
-        type Target = T;
 
-        fn deref(&self) -> &Self::Target {
-            &self.current_val
-        }
+    pub fn set(&self, new_val: T) {
+        self.modify(|f| *f = new_val);
     }
 
-    // enable displaty for the handle
-    impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> {
-        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-            write!(f, "{}", self.current_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<T> {
+    type Target = T;
 
-    /// 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! {
-    ///         <div>
-    ///             <h1>"Counter: {counter}" </h1>
-    ///             <button onclick={increment}> "Increment" </button>
-    ///             <button onclick={decrement}> "Decrement" </button>
-    ///         </div>  
-    ///     }
-    /// }
-    /// ```
-    pub fn use_state_new<'a, 'c, T: 'static, F: FnOnce() -> T>(
-        cx: &impl Scoped<'a>,
-        initial_state_fn: F,
-    ) -> &'a UseState<T> {
-        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> -> 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
-            },
-            |_| {},
-        )
+    fn deref(&self) -> &Self::Target {
+        &self.current_val
     }
 }
 
-mod use_ref_def {
-    use crate::innerlude::*;
-    use std::{cell::RefCell, ops::DerefMut};
-
-    // pub struct UseRef<T: 'static> {
-    //     _current: RefCell<T>,
-    // }
-    // impl<T: 'static> UseRef<T> {
-    //     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<T> {
-        cx.use_hook(|| RefCell::new(initial_state_fn()), |state| &*state, |_| {})
+// enable displaty for the handle
+impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.current_val)
     }
 }
 
-mod use_reducer_def {
-    use crate::innerlude::*;
-    use std::{cell::RefCell, ops::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! {
+///         <div>
+///             <h1>"Counter: {counter}" </h1>
+///             <button onclick={increment}> "Increment" </button>
+///             <button onclick={decrement}> "Decrement" </button>
+///         </div>  
+///     }
+/// }
+/// ```
+pub fn use_state_new<'a, 'c, T: 'static, F: FnOnce() -> T>(
+    cx: &impl Scoped<'a>,
+    initial_state_fn: F,
+) -> &'a UseState<T> {
+    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> -> 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
+        },
+        |_| {},
+    )
+}
 
-    struct UseReducer<T: 'static, R: 'static> {
-        new_val: Rc<RefCell<Option<T>>>,
-        current_val: T,
-        caller: Box<dyn Fn(R) + 'static>,
+// pub struct UseRef<T: 'static> {
+//     _current: RefCell<T>,
+// }
+// impl<T: 'static> UseRef<T> {
+//     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<T> {
+    cx.use_hook(|| RefCell::new(initial_state_fn()), |state| &*state, |_| {})
+}
+
+struct UseReducer<T: 'static, R: 'static> {
+    new_val: Rc<RefCell<Option<T>>>,
+    current_val: T,
+    caller: Box<dyn Fn(R) + 'static>,
+}
+
+/// 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<dyn Fn(Action)>) {
+    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> -> 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<Owned = T> + 'static>(
+    cx: &impl Scoped<'a>,
+    f: impl FnOnce() -> T,
+) -> &'a UseModel<T> {
+    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<T: ToOwned> {
+    real: T,
+    wip: RefCell<T>,
+    update: Rc<dyn Fn()>,
+}
+
+use std::cell::{Ref, RefMut};
+impl<T: ToOwned> Deref for UseModel<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.real
     }
+}
 
-    /// 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<dyn Fn(Action)>) {
-        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> -> 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)
-            },
-            |_| {},
-        )
+impl<T: Display + ToOwned> Display for UseModel<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.real)
     }
+}
 
-    // #[cfg(test)]
-    mod tests {
+impl<T: ToOwned> UseModel<T> {
+    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)
+    }
+}
 
-        use crate::prelude::*;
+// #[cfg(test)]
+mod tests {
 
-        enum Actions {
-            Incr,
-            Decr,
-        }
+    use crate::prelude::*;
 
-        // #[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)
-        //             }
-        //         }
-        //     })
-        // };
+    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<P>(cx: Context<P>) -> bool {

+ 3 - 0
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

+ 52 - 0
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,
+    },
+}

+ 59 - 53
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<Operator>);
-    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::<f64>().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<CalculatorDisplayProps>) -> 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::<f64>().unwrap().separated_string();
     // TODO: make it autoscaling with css
     cx.render(rsx! {
         div { class: "calculator-display"
-            "{formatted}"
+            div { class: "auto-scaling-text", "{display_value}" }
         }
     })
 }

+ 192 - 0
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<Operator>,
+    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::<f64>()
+            .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::<f64>().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::<f64>().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::<f64>().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),
+            _ => {}
+        }
+    }
+}

+ 14 - 0
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 

+ 5 - 3
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]

+ 2 - 2
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! {
             <div>
                 <h1> "Dioxus Desktop Demo" </h1>
                 <p> "Count is {count}"</p>
@@ -18,7 +18,7 @@ async fn main() {
                     "Click to increment"
                 </button>
             </div>
-       }
+       })
    })
    .configure_webview(|view| {
       // custom webview config options

+ 181 - 0
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<Operator>,
+    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::<f64>()
+        // .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::<f64>().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::<f64>().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::<f64>().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),
+            _ => {}
+        }
+    }
+}

+ 49 - 43
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! {
-        <div>
-        <svg class="octicon octicon-star v-align-text-bottom"
-        viewBox="0 0 14 16" version="1.1"
-        width="14" height="16"
-        xmlns="http://www.w3.org/2000/svg"
-        >
+// static Example: FC<()> = |cx| {
+//     cx.render(html! {
+//         <div>
+//         <svg class="octicon octicon-star v-align-text-bottom"
+//         viewBox="0 0 14 16" version="1.1"
+//         width="14" height="16"
+//         xmlns="http://www.w3.org/2000/svg"
+//         >
 
-        <path
-        d="M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14"
-        xmlns="http://www.w3.org/2000/svg"
-        >
-        </path>
+//         <path
+//         d="M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14"
+//         xmlns="http://www.w3.org/2000/svg"
+//         >
+//         </path>
 
-        </svg>
-        </div>
-    })
-};
-// 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"
-//                         }
-//                     }
-//                 }
-//             }
-//         }
+//         </svg>
+//         </div>
 //     })
 // };
+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}"
+                        }))}
+                    }
+                }
+            }
+        }
+    })
+};

+ 28 - 0
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 }
+        }
+    })
+};

+ 3 - 50
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<SerializedDom<'bump>>,
+    pub edits: Vec<DomEdits<'bump>>,
     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 });

+ 131 - 1
packages/webview/src/index.html

@@ -1 +1,131 @@
-<!-- this is used to instantiate the module -->
+<!-- a js-only interpreter for the dioxus patch stream :) -->
+<!DOCTYPE html>
+<html>
+
+<head>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
+    <meta charset="UTF-8" />
+    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
+</head>
+
+<body>
+    <div></div>
+</body>
+<script>
+    class Interpreter {
+        constructor(root) {
+            this.stack = [root];
+            this.lastNodeWasText = false;
+            this.nodes = {
+                0: root
+            };
+        }
+
+        top() {
+            return this.stack[this.stack.length - 1];
+        }
+
+        pop() {
+            return this.stack.pop();
+        }
+    }
+
+    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);
+        });
+    }
+
+    external.invoke("initiate");
+</script>
+
+</html>

+ 5 - 3
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<T: Properties + 'static>(
     builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder,
@@ -79,13 +79,15 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
             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");
                 }