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