diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0d8e9f3b4..968d5f103 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,7 +13,6 @@ jobs: runs-on: ubuntu-latest environment: docs steps: - - uses: actions/checkout@v3 # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released # - name: Setup mdBook @@ -25,6 +24,7 @@ jobs: - name: Setup mdBook run: | cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1 + - uses: actions/checkout@v3 - name: Build run: cd docs && diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index f6c06cd8c..c5616322e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,13 +30,13 @@ jobs: name: Test Suite runs-on: macos-latest steps: - - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b5e87ab71..de6b34274 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,6 @@ jobs: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -41,6 +40,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: check @@ -51,7 +51,6 @@ jobs: name: Test Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -63,6 +62,7 @@ jobs: - uses: davidB/rust-cargo-make@v1 - uses: browser-actions/setup-firefox@latest - uses: jetli/wasm-pack-action@v0.4.0 + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: make @@ -73,7 +73,6 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -81,6 +80,7 @@ jobs: override: true - uses: Swatinem/rust-cache@v2 - run: rustup component add rustfmt + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: fmt @@ -91,7 +91,6 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -101,6 +100,7 @@ jobs: - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy + - uses: actions/checkout@v3 - uses: actions-rs/cargo@v1 with: command: clippy diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index edfda431d..6ae99612f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -47,8 +47,6 @@ jobs: # which causes failures for some of rustfmt's line-ending sensitive tests - name: disable git eol translation run: git config --global core.autocrlf false - - name: checkout - uses: actions/checkout@v3 # Run build - name: Install Rustup using win.rustup.rs @@ -66,6 +64,9 @@ jobs: if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' shell: bash + - name: checkout + uses: actions/checkout@v3 + - name: test run: | rustc -Vv diff --git a/Cargo.toml b/Cargo.toml index 11ea7546b..8f3a33d96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ reqwest = { version = "0.11.9", features = ["json"] } fern = { version = "0.6.0", features = ["colored"] } thiserror = "1.0.30" env_logger = "0.9.0" +simple_logger = "4.0.0" [profile.release] opt-level = 3 diff --git a/examples/custom_element.rs b/examples/custom_element.rs index 87b1fac3f..6ed2e64cd 100644 --- a/examples/custom_element.rs +++ b/examples/custom_element.rs @@ -16,6 +16,12 @@ fn main() { } fn app(cx: Scope) -> Element { + let g = cx.component(component, (), "component"); + let c = cx.make_node(g); + cx.render(rsx! { + div { c } + }) + // let nf = NodeFactory::new(cx); // let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump()); @@ -27,6 +33,8 @@ fn app(cx: Scope) -> Element { // attrs.push(nf.attr("age", format_args!("47"), None, false)); // Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None)) +} +fn component(cx: Scope) -> Element { todo!() } diff --git a/examples/eval.rs b/examples/eval.rs index c0dda3e09..22e036b9c 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); @@ -7,6 +8,15 @@ fn main() { fn app(cx: Scope) -> Element { let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); + let future: &UseRef> = use_ref(cx, || None); + if future.read().is_some() { + let future_clone = future.clone(); + cx.spawn(async move { + if let Some(fut) = future_clone.with_mut(|o| o.take()) { + println!("{:?}", fut.await) + } + }); + } cx.render(rsx! { div { @@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element { oninput: move |e| script.set(e.value.clone()), } button { - onclick: move |_| eval(script.to_string()), + onclick: move |_| { + let fut = eval(script); + future.set(Some(fut)); + }, "Execute" } } diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index fb7bb623c..23ab19cf9 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -1,5 +1,4 @@ #![allow(non_snake_case)] - //! A tour of the rsx! macro //! ------------------------ //! @@ -39,7 +38,7 @@ //! - Accept a list of vnodes as children for a Fragment component //! - Allow keyed fragments in iterators //! - Allow top-level fragments -//! + fn main() { dioxus_desktop::launch(app); } diff --git a/examples/simple_desktop.rs b/examples/simple_desktop.rs new file mode 100644 index 000000000..d8c00afac --- /dev/null +++ b/examples/simple_desktop.rs @@ -0,0 +1,60 @@ +use dioxus::prelude::*; +use dioxus_router::*; + +fn main() { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Debug) + .with_module_level("dioxus_router", log::LevelFilter::Trace) + .with_module_level("dioxus", log::LevelFilter::Trace) + .init() + .unwrap(); + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + Router { + h1 { "Your app here" } + ul { + Link { to: "/", li { "home" } } + Link { to: "/blog", li { "blog" } } + Link { to: "/blog/tim", li { "tims' blog" } } + Link { to: "/blog/bill", li { "bills' blog" } } + Link { to: "/blog/james", + li { "james amazing' blog" } + } + Link { to: "/apples", li { "go to apples" } } + } + Route { to: "/", Home {} } + Route { to: "/blog/", BlogList {} } + Route { to: "/blog/:id/", BlogPost {} } + Route { to: "/oranges", "Oranges are not apples!" } + Redirect { from: "/apples", to: "/oranges" } + } + }) +} + +fn Home(cx: Scope) -> Element { + log::debug!("rendering home {:?}", cx.scope_id()); + cx.render(rsx! { h1 { "Home" } }) +} + +fn BlogList(cx: Scope) -> Element { + log::debug!("rendering blog list {:?}", cx.scope_id()); + cx.render(rsx! { div { "Blog List" } }) +} + +fn BlogPost(cx: Scope) -> Element { + let Some(id) = use_route(cx).segment("id") else { + return cx.render(rsx! { div { "No blog post id" } }) + }; + + log::debug!("rendering blog post {}", id); + + cx.render(rsx! { + div { + h3 { "blog post: {id:?}" } + Link { to: "/blog/", "back to blog list" } + } + }) +} diff --git a/examples/svg_basic.rs b/examples/svg_basic.rs index e950eb339..1264aa001 100644 --- a/examples/svg_basic.rs +++ b/examples/svg_basic.rs @@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element { stroke: "blue", stroke_width: "5", } + path { + d: "M9.00001 9C9 62 103.5 124 103.5 178", + stroke: "#3CC4DC", + "stroke-linecap": "square", + "stroke-width": "square", + } })) } diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index f9b97a436..e8dd1e1ef 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true proc-macro2 = { version = "1.0" } quote = "1.0" syn = { version = "1.0", features = ["full", "extra-traits"] } -dioxus-rsx = { path = "../rsx" } +dioxus-rsx = { path = "../rsx", version = "0.0.1" } # testing [dev-dependencies] diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 475000016..4e4f5600a 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token } /// The rsx! macro makes it easy for developers to write jsx-style markup in their components. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { match syn::parse::(s) { @@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream { /// The render! macro makes it easy for developers to write jsx-style markup in their components. /// /// The render macro automatically renders rsx - making it unhygenic. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn render(s: TokenStream) -> TokenStream { match syn::parse::(s) { diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f779e6ba2..03c120301 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] bumpalo = { version = "3.6", features = ["collections", "boxed"] } # faster hashmaps -fxhash = "0.2" +rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" @@ -35,6 +35,9 @@ indexmap = "1.7" serde = { version = "1", features = ["derive"], optional = true } anyhow = "1.0.66" +smallbox = "0.8.1" +log = "0.4.17" + [dev-dependencies] tokio = { version = "*", features = ["full"] } dioxus = { path = "../dioxus" } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 98960fca7..28d182b8f 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -34,14 +34,14 @@ impl ElementRef { impl VirtualDom { pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { - self.next(template, ElementPath::Deep(path)) + self.next_reference(template, ElementPath::Deep(path)) } pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { - self.next(template, ElementPath::Root(path)) + self.next_reference(template, ElementPath::Root(path)) } - fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId { + fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); @@ -95,22 +95,27 @@ impl VirtualDom { fn drop_scope_inner(&mut self, node: &VNode) { node.clear_listeners(); node.dynamic_nodes.iter().for_each(|node| match node { - DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()), + DynamicNode::Component(c) => { + if let Some(f) = c.scope.get() { + self.drop_scope(f) + } + } DynamicNode::Fragment(nodes) => { nodes.iter().for_each(|node| self.drop_scope_inner(node)) } DynamicNode::Placeholder(t) => { - self.try_reclaim(t.get()); + self.try_reclaim(t.id.get().unwrap()); } DynamicNode::Text(t) => { - self.try_reclaim(t.id.get()); + self.try_reclaim(t.id.get().unwrap()); } }); for root in node.root_ids { - let id = root.get(); - if id.0 != 0 { - self.try_reclaim(id); + if let Some(id) = root.get() { + if id.0 != 0 { + self.try_reclaim(id); + } } } } @@ -131,8 +136,10 @@ impl VirtualDom { node.dynamic_nodes.iter().for_each(|child| match child { // Only descend if the props are borrowed DynamicNode::Component(c) if !c.static_props => { - self.ensure_drop_safety(c.scope.get().unwrap()); - c.props.set(None); + if let Some(scope) = c.scope.get() { + self.ensure_drop_safety(scope); + } + c.props.take(); } DynamicNode::Fragment(f) => f diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 0f23151e6..fa102bd52 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,13 +1,15 @@ -use std::cell::Cell; -use std::rc::Rc; - -use crate::innerlude::{VComponent, VText}; +use crate::innerlude::{VComponent, VPlaceholder, VText}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext}; +use std::cell::Cell; +use std::iter::{Enumerate, Peekable}; +use std::rc::Rc; +use std::slice; +use TemplateNode::*; impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. @@ -17,187 +19,232 @@ impl<'b> VirtualDom { self.scope_stack.push(scope); let out = self.create(template); self.scope_stack.pop(); - out } /// Create this template and write its mutations - pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize { + pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize { // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways - if !self.templates.contains_key(&template.template.name) { - self.register_template(template); + if !self.templates.contains_key(&node.template.name) { + self.register_template(node); } + // we know that this will generate at least one mutation per node + self.mutations.edits.reserve(node.template.roots.len()); + // Walk the roots, creating nodes and assigning IDs // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS) - let mut dynamic_attrs = template.template.attr_paths.iter().enumerate().peekable(); - let mut dynamic_nodes = template.template.node_paths.iter().enumerate().peekable(); + let mut attrs = node.template.attr_paths.iter().enumerate().peekable(); + let mut nodes = node.template.node_paths.iter().enumerate().peekable(); - let cur_scope = self.scope_stack.last().copied().unwrap(); + node.template + .roots + .iter() + .enumerate() + .map(|(idx, root)| match root { + DynamicText { id } | Dynamic { id } => self.write_dynamic_root(node, *id), + Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes), + Text { .. } => self.write_static_text_root(node, idx), + }) + .sum() + } - let mut on_stack = 0; - for (root_idx, root) in template.template.roots.iter().enumerate() { - // We might need to generate an ID for the root node - on_stack += match root { - TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => { - match &template.dynamic_nodes[*id] { - // a dynamic text node doesn't replace a template node, instead we create it on the fly - DynamicNode::Text(VText { id: slot, value }) => { - let id = self.next_element(template, template.template.node_paths[*id]); - slot.set(id); + fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize { + // Simply just load the template root, no modifications needed + self.load_template_root(node, idx); - // Safety: we promise not to re-alias this text later on after committing it to the mutation - let unbounded_text = unsafe { std::mem::transmute(*value) }; - self.mutations.push(CreateTextNode { - value: unbounded_text, - id, - }); + // Text producs just one node on the stack + 1 + } - 1 - } + fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize { + use DynamicNode::*; + match &template.dynamic_nodes[idx] { + node @ Fragment(_) => self.create_dynamic_node(template, node, idx), + node @ Component { .. } => self.create_dynamic_node(template, node, idx), + Placeholder(VPlaceholder { id }) => { + let id = self.set_slot(template, id, idx); + self.mutations.push(CreatePlaceholder { id }); + 1 + } + Text(VText { id, value }) => { + let id = self.set_slot(template, id, idx); + self.create_static_text(value, id); + 1 + } + } + } - DynamicNode::Placeholder(slot) => { - let id = self.next_element(template, template.template.node_paths[*id]); - slot.set(id); - self.mutations.push(CreatePlaceholder { id }); - 1 - } + fn create_static_text(&mut self, value: &str, id: ElementId) { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_text: &str = unsafe { std::mem::transmute(value) }; + self.mutations.push(CreateTextNode { + value: unbounded_text, + id, + }); + } - DynamicNode::Fragment(_) | DynamicNode::Component { .. } => { - self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id) - } - } + /// We write all the descndent data for this element + /// + /// Elements can contain other nodes - and those nodes can be dynamic or static + /// + /// We want to make sure we write these nodes while on top of the root + fn write_element_root( + &mut self, + template: &'b VNode<'b>, + root_idx: usize, + dynamic_attrs: &mut Peekable>>, + dynamic_nodes: &mut Peekable>>, + ) -> usize { + // Load the template root and get the ID for the node on the stack + let root_on_stack = self.load_template_root(template, root_idx); + + // Write all the attributes below this root + self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template); + + // Load in all of the placeholder or dynamic content under this root too + self.load_placeholders(dynamic_nodes, root_idx, template); + + 1 + } + + /// Load all of the placeholder nodes for descendents of this root node + /// + /// ```rust, ignore + /// rsx! { + /// div { + /// // This is a placeholder + /// some_value, + /// + /// // Load this too + /// "{some_text}" + /// } + /// } + /// ``` + fn load_placeholders( + &mut self, + dynamic_nodes: &mut Peekable>>, + root_idx: usize, + template: &'b VNode<'b>, + ) { + let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) { + Some((a, b)) => (a, b), + None => return, + }; + + for idx in (start..=end).rev() { + let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); + if m > 0 { + // The path is one shorter because the top node is the root + let path = &template.template.node_paths[idx][1..]; + self.mutations.push(ReplacePlaceholder { m, path }); + } + } + } + + fn write_attrs_on_root( + &mut self, + attrs: &mut Peekable>>, + root_idx: usize, + root: ElementId, + node: &VNode, + ) { + while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) { + let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); + + loop { + self.write_attribute(&node.dynamic_attrs[attr_id], id); + + // Only push the dynamic attributes forward if they match the current path (same element) + match attrs.next_if(|(_, p)| *p == path) { + Some((next_attr_id, _)) => attr_id = next_attr_id, + None => break, } + } + } + } - TemplateNode::Element { .. } | TemplateNode::Text { .. } => { - let this_id = self.next_root(template, root_idx); + fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) { + // Make sure we set the attribute's associated id + attribute.mounted_element.set(id); - template.root_ids[root_idx].set(this_id); - self.mutations.push(LoadTemplate { - name: template.template.name, - index: root_idx, - id: this_id, - }); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_name = unsafe { std::mem::transmute(attribute.name) }; - // we're on top of a node that has a dynamic attribute for a descendant - // Set that attribute now before the stack gets in a weird state - while let Some((mut attr_id, path)) = - dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8) - { - // if attribute is on a root node, then we've already created the element - // Else, it's deep in the template and we should create a new id for it - let id = match path.len() { - 1 => this_id, - _ => { - let id = self - .next_element(template, template.template.attr_paths[attr_id]); - self.mutations.push(Mutation::AssignId { - path: &path[1..], - id, - }); - id - } - }; + match &attribute.value { + AttributeValue::Text(value) => { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_value = unsafe { std::mem::transmute(*value) }; - loop { - let attribute = template.dynamic_attrs.get(attr_id).unwrap(); - attribute.mounted_element.set(id); + self.mutations.push(SetAttribute { + name: unbounded_name, + value: unbounded_value, + ns: attribute.namespace, + id, + }) + } + AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute { + name: unbounded_name, + value: *value, + id, + }), + AttributeValue::Listener(_) => { + self.mutations.push(NewEventListener { + // all listeners start with "on" + name: &unbounded_name[2..], + id, + }) + } + AttributeValue::Float(_) => todo!(), + AttributeValue::Int(_) => todo!(), + AttributeValue::Any(_) => todo!(), + AttributeValue::None => todo!(), + } + } - // Safety: we promise not to re-alias this text later on after committing it to the mutation - let unbounded_name = unsafe { std::mem::transmute(attribute.name) }; + fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { + // Get an ID for this root since it's a real root + let this_id = self.next_root(template, root_idx); + template.root_ids[root_idx].set(Some(this_id)); - match &attribute.value { - AttributeValue::Text(value) => { - // Safety: we promise not to re-alias this text later on after committing it to the mutation - let unbounded_value = unsafe { std::mem::transmute(*value) }; + self.mutations.push(LoadTemplate { + name: template.template.name, + index: root_idx, + id: this_id, + }); - self.mutations.push(SetAttribute { - name: unbounded_name, - value: unbounded_value, - ns: attribute.namespace, - id, - }) - } - AttributeValue::Bool(value) => { - self.mutations.push(SetBoolAttribute { - name: unbounded_name, - value: *value, - id, - }) - } - AttributeValue::Listener(_) => { - self.mutations.push(NewEventListener { - // all listeners start with "on" - name: &unbounded_name[2..], - scope: cur_scope, - id, - }) - } - AttributeValue::Float(_) => todo!(), - AttributeValue::Int(_) => todo!(), - AttributeValue::Any(_) => todo!(), - AttributeValue::None => todo!(), - } + this_id + } - // Only push the dynamic attributes forward if they match the current path (same element) - match dynamic_attrs.next_if(|(_, p)| *p == path) { - Some((next_attr_id, _)) => attr_id = next_attr_id, - None => break, - } - } - } - - // We're on top of a node that has a dynamic child for a descendant - // Skip any node that's a root - let mut start = None; - let mut end = None; - - // Collect all the dynamic nodes below this root - // We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path - // - // [0] - // [1, 1] <---| - // [1, 1, 1] <---| these are the range of dynamic nodes below root 1 - // [1, 1, 2] <---| - // [2] - // - // We collect each range and then create them and replace the placeholder in the template - while let Some((idx, p)) = - dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) - { - if p.len() == 1 { - continue; - } - - if start.is_none() { - start = Some(idx); - } - - end = Some(idx); - } - - // - if let (Some(start), Some(end)) = (start, end) { - for idx in start..=end { - let node = &template.dynamic_nodes[idx]; - let m = self.create_dynamic_node(template, node, idx); - if m > 0 { - self.mutations.push(ReplacePlaceholder { - m, - path: &template.template.node_paths[idx][1..], - }); - } - } - } - - // elements create only one node :-) - 1 - } - }; + /// We have some dynamic attributes attached to a some node + /// + /// That node needs to be loaded at runtime, so we need to give it an ID + /// + /// If the node in question is on the stack, we just return that ID + /// + /// If the node is not on the stack, we create a new ID for it and assign it + fn assign_static_node_as_dynamic( + &mut self, + path: &'static [u8], + this_id: ElementId, + template: &VNode, + attr_id: usize, + ) -> ElementId { + if path.len() == 1 { + return this_id; } - on_stack + // if attribute is on a root node, then we've already created the element + // Else, it's deep in the template and we should create a new id for it + let id = self.next_element(template, template.template.attr_paths[attr_id]); + + self.mutations.push(Mutation::AssignId { + path: &path[1..], + id, + }); + + id } /// Insert a new template into the VirtualDom's template registry @@ -207,17 +254,9 @@ impl<'b> VirtualDom { .insert(template.template.name, template.template); // If it's all dynamic nodes, then we don't need to register it - // Quickly run through and see if it's all just dynamic nodes - if template.template.roots.iter().all(|root| { - matches!( - root, - TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. } - ) - }) { - return; + if !template.template.is_completely_dynamic() { + self.mutations.templates.push(template.template); } - - self.mutations.templates.push(template.template); } pub(crate) fn create_dynamic_node( @@ -245,7 +284,7 @@ impl<'b> VirtualDom { let new_id = self.next_element(template, template.template.node_paths[idx]); // Make sure the text node is assigned to the correct element - text.id.set(new_id); + text.id.set(Some(new_id)); // Safety: we promise not to re-alias this text later on after committing it to the mutation let value = unsafe { std::mem::transmute(text.value) }; @@ -263,7 +302,7 @@ impl<'b> VirtualDom { pub(crate) fn create_placeholder( &mut self, - slot: &Cell, + placeholder: &VPlaceholder, template: &'b VNode<'b>, idx: usize, ) -> usize { @@ -271,7 +310,7 @@ impl<'b> VirtualDom { let id = self.next_element(template, template.template.node_paths[idx]); // Make sure the text node is assigned to the correct element - slot.set(id); + placeholder.id.set(Some(id)); // Assign the ID to the existing node in the template self.mutations.push(AssignId { @@ -284,7 +323,7 @@ impl<'b> VirtualDom { } pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { - nodes.iter().fold(0, |acc, child| acc + self.create(child)) + nodes.iter().map(|child| self.create(child)).sum() } pub(super) fn create_component_node( @@ -293,15 +332,17 @@ impl<'b> VirtualDom { component: &'b VComponent<'b>, idx: usize, ) -> usize { - let props = component - .props - .replace(None) - .expect("Props to always exist when a component is being created"); + let scope = match component.props.take() { + Some(props) => { + let unbounded_props = unsafe { std::mem::transmute(props) }; + let scope = self.new_scope(unbounded_props, component.name); + scope.id + } - let unbounded_props = unsafe { std::mem::transmute(props) }; + // Component is coming back, it probably still exists, right? + None => component.scope.get().unwrap(), + }; - let scope = self.new_scope(unbounded_props, component.name); - let scope = scope.id; component.scope.set(Some(scope)); let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; @@ -391,4 +432,37 @@ impl<'b> VirtualDom { 0 } + + fn set_slot( + &mut self, + template: &'b VNode<'b>, + slot: &'b Cell>, + id: usize, + ) -> ElementId { + let id = self.next_element(template, template.template.node_paths[id]); + slot.set(Some(id)); + id + } +} + +fn collect_dyn_node_range( + dynamic_nodes: &mut Peekable>>, + root_idx: usize, +) -> Option<(usize, usize)> { + let start = match dynamic_nodes.peek() { + Some((idx, p)) if p[0] == root_idx as u8 => *idx, + _ => return None, + }; + + let mut end = start; + + while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) { + if p.len() == 1 { + continue; + } + + end = idx; + } + + Some((start, end)) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 0384192e2..8b283f7f5 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,17 +1,15 @@ -use std::cell::Cell; - use crate::{ arena::ElementId, - innerlude::{DirtyScope, VComponent, VText}, + innerlude::{DirtyScope, VComponent, VPlaceholder, VText}, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, scopes::ScopeId, virtual_dom::VirtualDom, - AttributeValue, TemplateNode, + Attribute, AttributeValue, TemplateNode, }; -use fxhash::{FxHashMap, FxHashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; use DynamicNode::*; impl<'b> VirtualDom { @@ -56,87 +54,101 @@ impl<'b> VirtualDom { fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {} fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { - if left_template.template.name != right_template.template.name { + // If the templates are the same, we don't need to do anything, nor do we want to + if templates_are_the_same(left_template, right_template) { + return; + } + + // If the templates are different by name, we need to replace the entire template + if templates_are_different(left_template, right_template) { return self.light_diff_templates(left_template, right_template); } - for (left_attr, right_attr) in left_template + // If the templates are the same, we can diff the attributes and children + // Start with the attributes + left_template .dynamic_attrs .iter() .zip(right_template.dynamic_attrs.iter()) - { - // Move over the ID from the old to the new - right_attr - .mounted_element - .set(left_attr.mounted_element.get()); + .for_each(|(left_attr, right_attr)| { + // Move over the ID from the old to the new + right_attr + .mounted_element + .set(left_attr.mounted_element.get()); - // We want to make sure anything listener that gets pulled is valid - if let AttributeValue::Listener(_) = right_attr.value { - self.update_template(left_attr.mounted_element.get(), right_template); - } - - if left_attr.value != right_attr.value || left_attr.volatile { - // todo: add more types of attribute values - match right_attr.value { - AttributeValue::Text(text) => { - let name = unsafe { std::mem::transmute(left_attr.name) }; - let value = unsafe { std::mem::transmute(text) }; - self.mutations.push(Mutation::SetAttribute { - id: left_attr.mounted_element.get(), - ns: right_attr.namespace, - name, - value, - }); - } - // todo: more types of attribute values - _ => todo!("other attribute types"), + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); } - } - } - for (idx, (left_node, right_node)) in left_template + // If the attributes are different (or volatile), we need to update them + if left_attr.value != right_attr.value || left_attr.volatile { + self.update_attribute(right_attr, left_attr); + } + }); + + // Now diff the dynamic nodes + left_template .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) .enumerate() - { - match (left_node, right_node) { - (Text(left), Text(right)) => self.diff_vtext(left, right), - (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), - (Placeholder(left), Placeholder(right)) => { - right.set(left.get()); - } - (Component(left), Component(right)) => { - self.diff_vcomponent(left, right, right_template, idx) - } - (Placeholder(left), Fragment(right)) => { - self.replace_placeholder_with_nodes(left, right) - } - (Fragment(left), Placeholder(right)) => { - self.replace_nodes_with_placeholder(left, right) - } - _ => todo!(), - }; - } + .for_each(|(idx, (left_node, right_node))| { + self.diff_dynamic_node(left_node, right_node, right_template, idx); + }); - // Make sure the roots get transferred over - for (left, right) in left_template + // Make sure the roots get transferred over while we're here + left_template .root_ids .iter() .zip(right_template.root_ids.iter()) - { - right.set(left.get()); + .for_each(|(left, right)| right.set(left.get())); + } + + fn diff_dynamic_node( + &mut self, + left_node: &'b DynamicNode<'b>, + right_node: &'b DynamicNode<'b>, + node: &'b VNode<'b>, + idx: usize, + ) { + match (left_node, right_node) { + (Text(left), Text(right)) => self.diff_vtext(left, right, node), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), + (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right), + (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), + _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), + }; + } + + fn update_attribute(&mut self, right_attr: &Attribute, left_attr: &Attribute) { + // todo: add more types of attribute values + match right_attr.value { + AttributeValue::Text(text) => { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value = unsafe { std::mem::transmute(text) }; + self.mutations.push(Mutation::SetAttribute { + id: left_attr.mounted_element.get(), + ns: right_attr.namespace, + name, + value, + }); + } + // todo: more types of attribute values + _ => todo!("other attribute types"), } } - fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { + fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { let m = self.create_children(r); - let id = l.get(); + let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); self.reclaim(id); } - fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { // Remove the old nodes, except for one self.remove_nodes(&l[1..]); @@ -145,7 +157,7 @@ impl<'b> VirtualDom { // Create the placeholder first, ensuring we get a dedicated ID for the placeholder let placeholder = self.next_element(&l[0], &[]); - r.set(placeholder); + r.id.set(Some(placeholder)); self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); @@ -162,6 +174,10 @@ impl<'b> VirtualDom { right_template: &'b VNode<'b>, idx: usize, ) { + if std::ptr::eq(left, right) { + return; + } + // Replace components that have different render fns if left.render_fn != right.render_fn { let created = self.create_component_node(right_template, right, idx); @@ -181,12 +197,22 @@ impl<'b> VirtualDom { } // Make sure the new vcomponent has the right scopeid associated to it - let scope_id = left.scope.get().unwrap(); + let Some(scope_id) = left.scope.get() else { + return; + }; + // let scope_id = left.scope.get().unwrap_or_else(|| { + // panic!( + // "A component should always have a scope associated to it. {:?}\n {:#?}", + // right.name, + // std::backtrace::Backtrace::force_capture() + // ) + // }); + right.scope.set(Some(scope_id)); // copy out the box for both let old = self.scopes[scope_id.0].props.as_ref(); - let new = right.props.replace(None).unwrap(); + let new = right.props.take().unwrap(); // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything @@ -201,6 +227,11 @@ impl<'b> VirtualDom { // Now run the component and diff it self.run_scope(scope_id); self.diff_scope(scope_id); + + self.dirty_scopes.remove(&DirtyScope { + height: self.scopes[scope_id.0].height, + id: scope_id, + }); } /// Lightly diff the two templates, checking only their roots. @@ -254,10 +285,13 @@ impl<'b> VirtualDom { /// /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. - fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { - let id = left.id.get(); + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { + let id = left + .id + .get() + .unwrap_or_else(|| self.next_element(node, &[0])); - right.id.set(id); + right.id.set(Some(id)); if left.value != right.value { let value = unsafe { std::mem::transmute(right.value) }; self.mutations.push(Mutation::SetText { id, value }); @@ -269,9 +303,9 @@ impl<'b> VirtualDom { /// All IDs will be garbage collected fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { let id = match node.dynamic_root(0) { - None => node.root_ids[0].get(), - Some(Text(t)) => t.id.get(), - Some(Placeholder(e)) => e.get(), + None => node.root_ids[0].get().unwrap(), + Some(Text(t)) => t.id.get().unwrap(), + Some(Placeholder(e)) => e.id.get().unwrap(), Some(Fragment(nodes)) => { let id = self.replace_inner(&nodes[0]); self.remove_nodes(&nodes[1..]); @@ -309,14 +343,23 @@ impl<'b> VirtualDom { match dyn_node { Component(comp) => { - let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.clean_up_node(t), - _ => todo!("cannot handle nonstandard nodes"), - }; + if let Some(scope) = comp.scope.take() { + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.clean_up_node(t), + _ => todo!("cannot handle nonstandard nodes"), + }; + } + } + Text(t) => { + if let Some(id) = t.id.take() { + self.reclaim(id) + } + } + Placeholder(t) => { + if let Some(id) = t.id.take() { + self.reclaim(id) + } } - Text(t) => self.reclaim(t.id.get()), - Placeholder(t) => self.reclaim(t.get()), Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)), }; } @@ -344,25 +387,25 @@ impl<'b> VirtualDom { fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { match node.dynamic_root(idx) { Some(Text(i)) => { - let id = i.id.get(); + let id = i.id.take().unwrap(); self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } Some(Placeholder(e)) => { - let id = e.get(); + let id = e.id.take().unwrap(); self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } Some(Fragment(nodes)) => self.remove_nodes(nodes), Some(Component(comp)) => { - let scope = comp.scope.get().unwrap(); + let scope = comp.scope.take().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(t)) => self.remove_node(t), _ => todo!("cannot handle nonstandard nodes"), }; } None => { - let id = node.root_ids[idx].get(); + let id = node.root_ids[idx].get().unwrap(); self.mutations.push(Mutation::Remove { id }); self.reclaim(id); } @@ -432,7 +475,7 @@ impl<'b> VirtualDom { // The stack is empty upon entry. fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); + let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { @@ -748,12 +791,16 @@ impl<'b> VirtualDom { fn remove_node(&mut self, node: &'b VNode<'b>) { for (idx, _) in node.template.roots.iter().enumerate() { let id = match node.dynamic_root(idx) { - Some(Text(t)) => t.id.get(), - Some(Placeholder(t)) => t.get(), + Some(Text(t)) => t.id.take(), + Some(Placeholder(t)) => t.id.take(), Some(Fragment(t)) => return self.remove_nodes(t), - Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()), + Some(Component(comp)) => { + comp.scope.set(None); + return self.remove_component(comp.scope.get().unwrap()); + } None => node.root_ids[idx].get(), - }; + } + .unwrap(); self.mutations.push(Mutation::Remove { id }) } @@ -761,7 +808,7 @@ impl<'b> VirtualDom { self.clean_up_node(node); for root in node.root_ids { - let id = root.get(); + let id = root.get().unwrap(); if id.0 != 0 { self.reclaim(id); } @@ -784,41 +831,45 @@ impl<'b> VirtualDom { /// Push all the real nodes on the stack fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { - let mut onstack = 0; - - for (idx, _) in node.template.roots.iter().enumerate() { - match node.dynamic_root(idx) { - Some(Text(t)) => { - self.mutations.push(Mutation::PushRoot { id: t.id.get() }); - onstack += 1; - } - Some(Placeholder(t)) => { - self.mutations.push(Mutation::PushRoot { id: t.get() }); - onstack += 1; - } - Some(Fragment(nodes)) => { - for node in *nodes { - onstack += self.push_all_real_nodes(node); + node.template + .roots + .iter() + .enumerate() + .map(|(idx, _)| { + match node.dynamic_root(idx) { + Some(Text(t)) => { + self.mutations.push(Mutation::PushRoot { + id: t.id.get().unwrap(), + }); + 1 } - } - Some(Component(comp)) => { - let scope = comp.scope.get().unwrap(); - onstack += + Some(Placeholder(t)) => { + self.mutations.push(Mutation::PushRoot { + id: t.id.get().unwrap(), + }); + 1 + } + Some(Fragment(nodes)) => nodes + .iter() + .map(|node| self.push_all_real_nodes(node)) + .count(), + + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), _ => todo!(), } - } - None => { - self.mutations.push(Mutation::PushRoot { - id: node.root_ids[idx].get(), - }); - onstack += 1; - } - }; - } - - onstack + } + None => { + self.mutations.push(Mutation::PushRoot { + id: node.root_ids[idx].get().unwrap(), + }); + 1 + } + }; + }) + .count() } fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { @@ -839,10 +890,10 @@ impl<'b> VirtualDom { fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { - None => node.root_ids[0].get(), - Some(Text(t)) => t.id.get(), + None => node.root_ids[0].get().unwrap(), + Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_first_element(&t[0]), - Some(Placeholder(t)) => t.get(), + Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -855,10 +906,10 @@ impl<'b> VirtualDom { fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(node.template.roots.len() - 1) { - None => node.root_ids.last().unwrap().get(), - Some(Text(t)) => t.id.get(), + None => node.root_ids.last().unwrap().get().unwrap(), + Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), - Some(Placeholder(t)) => t.get(), + Some(Placeholder(t)) => t.id.get().unwrap(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -892,6 +943,21 @@ impl<'b> VirtualDom { } } +/// Are the templates the same? +/// +/// We need to check for the obvious case, and the non-obvious case where the template as cloned +/// +/// We use the pointer of the dynamic_node list in this case +fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool { + std::ptr::eq(left_template, right_template) + || std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes) +} + +fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool { + !std::ptr::eq(left_template.template.name, right_template.template.name) + && left_template.template.name != right_template.template.name +} + fn matching_components<'a>( left: &'a VNode<'a>, right: &'a VNode<'a>, diff --git a/packages/core/src/dirty_scope.rs b/packages/core/src/dirty_scope.rs index 4dfbd9bd3..87fff3f45 100644 --- a/packages/core/src/dirty_scope.rs +++ b/packages/core/src/dirty_scope.rs @@ -1,19 +1,21 @@ +use std::hash::Hash; + use crate::ScopeId; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Eq, PartialOrd, Ord)] pub struct DirtyScope { pub height: u32, pub id: ScopeId, } -impl PartialOrd for DirtyScope { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.height.cmp(&other.height)) +impl PartialEq for DirtyScope { + fn eq(&self, other: &Self) -> bool { + self.id == other.id } } -impl Ord for DirtyScope { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.height.cmp(&other.height) +impl Hash for DirtyScope { + fn hash(&self, state: &mut H) { + self.id.hash(state); } } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 020b307d9..eca7829dc 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -11,8 +11,9 @@ //! The logic for this was borrowed from . Unfortunately, this crate does not //! support non-static closures, so we've implemented the core logic of `ValueA` in this module. +use smallbox::{smallbox, space::S16, SmallBox}; + use crate::{innerlude::VNode, ScopeState}; -use std::mem; /// A concrete type provider for closures that build [`VNode`] structures. /// @@ -24,14 +25,7 @@ use std::mem; /// LazyNodes::new(|f| f.element("div", [], [], [] None)) /// ``` pub struct LazyNodes<'a, 'b> { - inner: StackNodeStorage<'a, 'b>, -} - -type StackHeapSize = [usize; 16]; - -enum StackNodeStorage<'a, 'b> { - Stack(LazyStack), - Heap(Box) -> Option> + 'b>), + inner: SmallBox VNode<'a> + 'b, S16>, } impl<'a, 'b> LazyNodes<'a, 'b> { @@ -44,114 +38,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(val); - let val = move |fac: Option<&'a ScopeState>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - }; - - // miri does not know how to work with mucking directly into bytes - // just use a heap allocated type when miri is running - if cfg!(miri) { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - unsafe { LazyNodes::new_inner(val) } - } - } - - /// Create a new [`LazyNodes`] closure, but force it onto the heap. - pub fn new_boxed(inner: F) -> Self - where - F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b, - { - // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch - let mut slot = Some(inner); - Self { - inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - })), - } - } - - unsafe fn new_inner(val: F) -> Self - where - F: FnMut(Option<&'a ScopeState>) -> Option> + 'b, - { - let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option>; - - assert_eq!( - ptr as *const u8, &val as *const _ as *const u8, - "MISUSE: Closure returned different pointer" - ); - assert_eq!( - std::mem::size_of_val(&*ptr), - std::mem::size_of::(), - "MISUSE: Closure returned a subset pointer" - ); - - let words = ptr_as_slice(&mut ptr); - assert!( - words[0] == &val as *const _ as usize, - "BUG: Pointer layout is not (data_ptr, info...)" - ); - - // - Ensure that Self is aligned same as data requires - assert!( - std::mem::align_of::() <= std::mem::align_of::(), - "TODO: Enforce alignment >{} (requires {})", - std::mem::align_of::(), - std::mem::align_of::() - ); - - let info = &words[1..]; - let data = words[0] as *mut (); - let size = mem::size_of::(); - - let stored_size = info.len() * mem::size_of::() + size; - let max_size = mem::size_of::(); - - if stored_size > max_size { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - let mut buf: StackHeapSize = StackHeapSize::default(); - - assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); - - // Place pointer information at the end of the region - // - Allows the data to be at the start for alignment purposes - { - let info_ofs = buf.as_ref().len() - info.len(); - let info_dst = &mut buf.as_mut()[info_ofs..]; - for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { - *d = *v; - } - } - - let src_ptr = data as *const u8; - let dataptr = buf.as_mut_ptr().cast::(); - - for i in 0..size { - *dataptr.add(i) = *src_ptr.add(i); - } - - std::mem::forget(val); - - Self { - inner: StackNodeStorage::Stack(LazyStack { - _align: [], - buf, - dropped: false, - }), - } + inner: smallbox!(move |f| { + let val = slot.take().expect("cannot call LazyNodes twice"); + val(f) + }), } } @@ -163,88 +54,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// let node = f.call(cac); /// ``` #[must_use] - pub fn call(self, f: &'a ScopeState) -> VNode<'a> { - match self.inner { - StackNodeStorage::Heap(mut lazy) => { - lazy(Some(f)).expect("Closure should not be called twice") - } - StackNodeStorage::Stack(mut stack) => stack.call(f), - } + pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> { + (self.inner)(f) } } - -struct LazyStack { - _align: [u64; 0], - buf: StackHeapSize, - dropped: bool, -} - -impl LazyStack { - fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = - mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option>>() - / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(Some(f)).unwrap() - } -} -impl Drop for LazyStack { - fn drop(&mut self) { - if !self.dropped { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = - mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option>>() - / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option<&ScopeState>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(None); - } - } -} - -/// Obtain mutable access to a pointer's words -fn ptr_as_slice(ptr: &mut T) -> &mut [usize] { - assert!(mem::size_of::() % mem::size_of::() == 0); - let words = mem::size_of::() / mem::size_of::(); - // SAFE: Points to valid memory (a raw pointer) - unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) } -} - -/// Re-construct a fat pointer -unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut T { - let mut rv = mem::MaybeUninit::<*mut T>::uninit(); - { - let s = ptr_as_slice(&mut rv); - s[0] = data_ptr; - s[1..].copy_from_slice(meta_vals); - } - let rv = rv.assume_init(); - assert_eq!(rv as *const (), data_ptr as *const ()); - rv -} - -fn round_to_words(len: usize) -> usize { - (len + mem::size_of::() - 1) / mem::size_of::() -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 613a4a115..7ce2b723f 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -81,9 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, - ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode, - VirtualDom, + fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, + Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, + VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 2512ae9fa..f8553cd46 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,4 +1,4 @@ -use fxhash::FxHashSet; +use rustc_hash::FxHashSet; use crate::{arena::ElementId, ScopeId, Template}; @@ -230,9 +230,6 @@ pub enum Mutation<'a> { /// The name of the event to listen for. name: &'a str, - /// The ID of the node to attach the listener to. - scope: ScopeId, - /// The ID of the node to attach the listener to. id: ElementId, }, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index ee0adccfa..0e6738b2b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -46,7 +46,7 @@ pub struct VNode<'a> { /// The IDs for the roots of this template - to be used when moving the template around and removing it from /// the actual Dom - pub root_ids: &'a [Cell], + pub root_ids: &'a [Cell>], /// The dynamic parts of the template pub dynamic_nodes: &'a [DynamicNode<'a>], @@ -145,6 +145,18 @@ where Ok(&*Box::leak(deserialized)) } +impl<'a> Template<'a> { + /// Is this template worth caching at all, since it's completely runtime? + /// + /// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway. + pub fn is_completely_dynamic(&self) -> bool { + use TemplateNode::*; + self.roots + .iter() + .all(|root| matches!(root, Dynamic { .. } | DynamicText { .. })) + } +} + /// A statically known node in a layout. /// /// This can be created at compile time, saving the VirtualDom time when diffing the tree @@ -224,7 +236,7 @@ pub enum DynamicNode<'a> { /// Used by suspense when a node isn't ready and by fragments that don't render anything /// /// In code, this is just an ElementId whose initial value is set to 0 upon creation - Placeholder(Cell), + Placeholder(VPlaceholder), /// A list of VNodes. /// @@ -259,7 +271,7 @@ pub struct VComponent<'a> { /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key pub render_fn: *const (), - pub(crate) props: Cell + 'a>>>, + pub(crate) props: RefCell + 'a>>>, } impl<'a> std::fmt::Debug for VComponent<'a> { @@ -279,7 +291,14 @@ pub struct VText<'a> { pub value: &'a str, /// The ID of this node in the real DOM - pub id: Cell, + pub id: Cell>, +} + +/// A placeholder node, used by suspense and fragments +#[derive(Debug, Default)] +pub struct VPlaceholder { + /// The ID of this node in the real DOM + pub id: Cell>, } /// An attribute of the TemplateNode, created at compile time @@ -464,6 +483,12 @@ impl<'a> IntoDynNode<'a> for VNode<'a> { } } +impl<'a> IntoDynNode<'a> for DynamicNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + self + } +} + // An element that's an error is currently lost into the ether impl<'a> IntoDynNode<'a> for Element<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index af36f1aa6..db328fd93 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -8,6 +8,7 @@ use crate::{ scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; +use bumpalo::Bump; use futures_util::FutureExt; use std::{ mem, @@ -34,8 +35,8 @@ impl VirtualDom { props: Some(props), name, placeholder: Default::default(), - node_arena_1: BumpFrame::new(50), - node_arena_2: BumpFrame::new(50), + node_arena_1: BumpFrame::new(0), + node_arena_2: BumpFrame::new(0), spawned_tasks: Default::default(), render_cnt: Default::default(), hook_arena: Default::default(), @@ -62,7 +63,13 @@ impl VirtualDom { let mut new_nodes = unsafe { let scope = self.scopes[scope_id.0].as_mut(); - scope.previous_frame_mut().bump.reset(); + // if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient + if scope.previous_frame().bump.allocated_bytes() == 0 { + scope.previous_frame_mut().bump = + Bump::with_capacity(scope.current_frame().bump.allocated_bytes()); + } else { + scope.previous_frame_mut().bump.reset(); + } // Make sure to reset the hook counter so we give out hooks in the right order scope.hook_idx.set(0); @@ -128,8 +135,8 @@ impl VirtualDom { let frame = scope.previous_frame(); // set the new head of the bump frame - let alloced = &*frame.bump.alloc(new_nodes); - frame.node.set(alloced); + let allocated = &*frame.bump.alloc(new_nodes); + frame.node.set(allocated); // And move the render generation forward by one scope.render_cnt.set(scope.render_cnt.get() + 1); @@ -141,6 +148,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(alloced) } + unsafe { mem::transmute(allocated) } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index b1bbb42fa..0e487bfa8 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -10,10 +10,10 @@ use crate::{ Attribute, AttributeValue, Element, Event, Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; +use rustc_hash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - collections::{HashMap, HashSet}, fmt::Arguments, future::Future, rc::Rc, @@ -82,10 +82,10 @@ pub struct ScopeState { pub(crate) hook_list: RefCell>, pub(crate) hook_idx: Cell, - pub(crate) shared_contexts: RefCell>>, + pub(crate) shared_contexts: RefCell>>, pub(crate) tasks: Rc, - pub(crate) spawned_tasks: HashSet, + pub(crate) spawned_tasks: FxHashSet, pub(crate) props: Option>>, pub(crate) placeholder: Cell>, @@ -237,7 +237,9 @@ impl<'src> ScopeState { /// This method should be used when you want to schedule an update for a component pub fn schedule_update_any(&self) -> Arc { let chan = self.tasks.sender.clone(); - Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) + Arc::new(move |id| { + chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap(); + }) } /// Mark this scope as dirty, and schedule a render for it. @@ -448,7 +450,7 @@ impl<'src> ScopeState { name: fn_name, render_fn: component as *const (), static_props: P::IS_STATIC, - props: Cell::new(Some(extended)), + props: RefCell::new(Some(extended)), scope: Cell::new(None), }) } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 01f11efef..44f085b09 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -14,15 +14,9 @@ use crate::{ AttributeValue, Element, Event, Scope, SuspenseContext, }; use futures_util::{pin_mut, StreamExt}; +use rustc_hash::FxHashMap; use slab::Slab; -use std::{ - any::Any, - borrow::BorrowMut, - cell::Cell, - collections::{BTreeSet, HashMap}, - future::Future, - rc::Rc, -}; +use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -148,7 +142,7 @@ use std::{ /// } /// ``` pub struct VirtualDom { - pub(crate) templates: HashMap>, + pub(crate) templates: FxHashMap>, pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -542,6 +536,8 @@ impl VirtualDom { pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { pin_mut!(deadline); + self.process_events(); + loop { // first, unload any complete suspense trees for finished_fiber in self.finished_fibers.drain(..) { diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 78ef608cc..fca6fcbaa 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -34,7 +34,7 @@ fn dual_stream() { [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, - NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) }, + NewEventListener { name: "click", id: ElementId(1) }, HydrateText { path: &[0, 0], value: "123", id: ElementId(2) }, AppendChildren { id: ElementId(0), m: 1 } ], diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs index 19d3a44a0..faf783603 100644 --- a/packages/core/tests/miri_simple.rs +++ b/packages/core/tests/miri_simple.rs @@ -62,8 +62,8 @@ fn contexts_drop() { _ = dom.render_immediate(); } -#[test] -fn tasks_drop() { +#[tokio::test] +async fn tasks_drop() { fn app(cx: Scope) -> Element { cx.spawn(async { tokio::time::sleep(std::time::Duration::from_millis(100000)).await; diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 2d9760e5d..160baa25d 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -12,9 +12,7 @@ fn test_memory_leak() { fn app(cx: Scope) -> Element { let val = cx.generation(); - cx.spawn(async { - tokio::time::sleep(std::time::Duration::from_millis(100000)).await; - }); + cx.spawn(async {}); if val == 2 || val == 4 { return cx.render(rsx!(())); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 58e947574..4aaae6c64 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -20,7 +20,7 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -wry = { version = "0.22.0" } +wry = { version = "0.23.4" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ "sync", diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 445c197b8..2ae353d57 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,10 +1,12 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use crate::events::{decode_event, EventMessage}; +use crate::events::IpcMessage; use dioxus_core::*; +use dioxus_html::{Html, HtmlEvent}; use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use futures_util::StreamExt; #[cfg(target_os = "ios")] use objc::runtime::Object; +use serde_json::Value; use std::{ collections::HashMap, sync::Arc, @@ -19,11 +21,12 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, + pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, pub(super) pending_edits: Arc>>, pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, pub(super) proxy: EventLoopProxy, - pub(super) event_tx: UnboundedSender, + pub(super) event_tx: UnboundedSender, #[cfg(debug_assertions)] pub(super) templates_tx: UnboundedSender>, @@ -40,12 +43,13 @@ impl DesktopController { proxy: EventLoopProxy, ) -> Self { let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (event_tx, mut event_rx) = unbounded(); + let (event_tx, mut event_rx) = unbounded::(); let (templates_tx, mut templates_rx) = unbounded(); let proxy2 = proxy.clone(); let pending_edits = edit_queue.clone(); let desktop_context_proxy = proxy.clone(); + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads @@ -57,7 +61,7 @@ impl DesktopController { runtime.block_on(async move { let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy)); + .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); { let edits = dom.rebuild(); let mut queue = edit_queue.lock().unwrap(); @@ -80,14 +84,8 @@ impl DesktopController { dom.replace_template(template); } _ = dom.wait_for_work() => {} - Some(json_value) = event_rx.next() => { - if let Ok(value) = serde_json::from_value::(json_value) { - let name = value.event.clone(); - let el_id = ElementId(value.mounted_dom_id); - if let Some(evt) = decode_event(value) { - dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); - } - } + Some(value) = event_rx.next() => { + dom.handle_event(&value.name, value.data.into_any(), value.element, dioxus_html::events::event_bubbles(&value.name)); } } @@ -103,6 +101,7 @@ impl DesktopController { Self { pending_edits, + eval_sender, webviews: HashMap::new(), is_ready: Arc::new(AtomicBool::new(false)), quit_app_on_close: true, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 66e2d1488..6b6d02ba3 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -2,6 +2,11 @@ use std::rc::Rc; use crate::controller::DesktopController; use dioxus_core::ScopeState; +use serde::de::Error; +use serde_json::Value; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &Rc { - let desktop = use_window(cx); - - &*cx.use_hook(|| { - let desktop = desktop.clone(); - Rc::new(move |script| desktop.eval(script)) - } as Rc) -} - /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc { pub struct DesktopContext { /// The wry/tao proxy to the current window pub proxy: ProxyType, + pub(super) eval_reciever: Rc>>, } impl DesktopContext { - pub(crate) fn new(proxy: ProxyType) -> Self { - Self { proxy } + pub(crate) fn new( + proxy: ProxyType, + eval_reciever: tokio::sync::mpsc::UnboundedReceiver, + ) -> Self { + Self { + proxy, + eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + } } /// trigger the drag-window event @@ -236,6 +238,18 @@ impl DesktopController { Resizable(state) => window.set_resizable(state), AlwaysOnTop(state) => window.set_always_on_top(state), + Eval(code) => { + let script = format!( + r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ + {} + }})()}}));"#, + code + ); + if let Err(e) = webview.evaluate_script(&script) { + // we can't panic this error. + log::warn!("Eval script error: {e}"); + } + } CursorVisible(state) => window.set_cursor_visible(state), CursorGrab(state) => { let _ = window.set_cursor_grab(state); @@ -259,13 +273,6 @@ impl DesktopController { log::warn!("Devtools are disabled in release builds"); } - Eval(code) => { - if let Err(e) = webview.evaluate_script(code.as_str()) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - #[cfg(target_os = "ios")] PushView(view) => unsafe { use objc::runtime::Object; @@ -295,6 +302,39 @@ impl DesktopController { } } +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { + let desktop = use_window(cx).clone(); + cx.use_hook(|| { + move |script| { + desktop.eval(script); + let recv = desktop.eval_reciever.clone(); + EvalResult { reciever: recv } + } + }) +} + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + reciever: Rc>>, +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.reciever.lock().await; + match reciever.recv().await { + Some(result) => Ok(result), + None => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + #[cfg(target_os = "ios")] fn is_main_thread() -> bool { use objc::runtime::{Class, BOOL, NO}; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index b82d6f749..2496d6ddb 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -6,7 +6,7 @@ use serde_json::from_value; use std::any::Any; use std::rc::Rc; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub(crate) struct IpcMessage { method: String, params: serde_json::Value, @@ -31,61 +31,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option { } } } - -macro_rules! match_data { - ( - $m:ident; - $name:ident; - $( - $tip:ty => $($mname:literal)|* ; - )* - ) => { - match $name { - $( $($mname)|* => { - let val: $tip = from_value::<$tip>($m).ok()?; - Rc::new(val) as Rc - })* - _ => return None, - } - }; -} - -#[derive(Deserialize)] -pub struct EventMessage { - pub contents: serde_json::Value, - pub event: String, - pub mounted_dom_id: usize, -} - -pub fn decode_event(value: EventMessage) -> Option> { - let val = value.contents; - let name = value.event.as_str(); - type DragData = MouseData; - - let evt = match_data! { val; name; - MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup"; - ClipboardData => "copy" | "cut" | "paste"; - CompositionData => "compositionend" | "compositionstart" | "compositionupdate"; - KeyboardData => "keydown" | "keypress" | "keyup"; - FocusData => "blur" | "focus" | "focusin" | "focusout"; - FormData => "change" | "input" | "invalid" | "reset" | "submit"; - DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop"; - PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture"; - SelectionData => "selectstart" | "selectionchange" | "select"; - TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart"; - ScrollData => "scroll"; - WheelData => "wheel"; - MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" - | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata" - | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" - | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" - | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout"; - AnimationData => "animationstart" | "animationend" | "animationiteration"; - TransitionData => "transitionend"; - ToggleData => "toggle"; - // ImageData => "load" | "error"; - // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; - }; - - Some(evt) -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index c858c0072..e14cf9235 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -17,7 +17,8 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext}; +pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; +use dioxus_html::HtmlEvent; use futures_channel::mpsc::UnboundedSender; pub use wry; pub use wry::application as tao; @@ -145,6 +146,7 @@ impl DesktopController { event_loop, self.is_ready.clone(), self.proxy.clone(), + self.eval_sender.clone(), self.event_tx.clone(), ); @@ -157,7 +159,8 @@ fn build_webview( event_loop: &tao::event_loop::EventLoopWindowTarget, is_ready: Arc, proxy: tao::event_loop::EventLoopProxy, - event_tx: UnboundedSender, + eval_sender: tokio::sync::mpsc::UnboundedSender, + event_tx: UnboundedSender, ) -> wry::webview::WebView { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); @@ -186,8 +189,14 @@ fn build_webview( .with_ipc_handler(move |_window: &Window, payload: String| { parse_ipc_message(&payload) .map(|message| match message.method() { + "eval_result" => { + let result = message.params(); + eval_sender.send(result).unwrap(); + } "user_event" => { - _ = event_tx.unbounded_send(message.params()); + if let Ok(evt) = serde_json::from_value(message.params()) { + _ = event_tx.unbounded_send(evt); + } } "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); @@ -234,16 +243,16 @@ fn build_webview( // in release mode, we don't want to show the dev tool or reload menus webview = webview.with_initialization_script( r#" - if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#, ) } else { // in debug, we are okay with the reload menu showing and dev tool diff --git a/packages/desktop/src/main.js b/packages/desktop/src/main.js new file mode 100644 index 000000000..c1f0cf9f4 --- /dev/null +++ b/packages/desktop/src/main.js @@ -0,0 +1,7 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.ipc.postMessage(serializeIpcMessage("initialize")); + } +} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index cc084b403..f9c96cfea 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -52,7 +52,14 @@ pub(super) fn desktop_handler( } else if trimmed == "index.js" { Response::builder() .header("Content-Type", "text/javascript") - .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) + .body( + format!( + "{} {}", + dioxus_interpreter_js::INTERPRETER_JS, + include_str!("./main.js") + ) + .into_bytes(), + ) .map_err(From::from) } else { let asset_root = asset_root @@ -104,10 +111,10 @@ fn get_asset_root() -> Option { #[cfg(target_os = "macos")] { let bundle = core_foundation::bundle::CFBundle::main_bundle(); - let bundle_path = dbg!(bundle.path()?); - let resources_path = dbg!(bundle.resources_path()?); - let absolute_resources_root = dbg!(bundle_path.join(resources_path)); - let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?); + let bundle_path = bundle.path()?; + let resources_path = bundle.resources_path()?; + let absolute_resources_root = bundle_path.join(resources_path); + let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; return Some(canonical_resources_root); } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 86adae336..fed3d2bd5 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -1,5 +1,3 @@ -#![doc = include_str!("../../../notes/README.md")] - pub use dioxus_core as core; #[cfg(feature = "hooks")] diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index db386a6ea..1d785b7f1 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -40,7 +40,10 @@ features = [ "ClipboardEvent", ] +[dev-dependencies] +serde_json = "*" + [features] -default = [] -serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"] +default = ["serialize"] +serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"] wasm-bind = ["web-sys", "wasm-bindgen"] diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 5dd7f5c71..53c118a77 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -10,12 +10,11 @@ pub type DragEvent = Event; /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] pub struct DragData { /// Inherit mouse data pub mouse: MouseData, - - /// And then add the rest of the drag data - pub data: Box, } impl_event! { diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 5e0b6a17e..1d995fa09 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -11,9 +11,8 @@ pub struct FormData { pub value: String, pub values: HashMap, - - #[cfg_attr(feature = "serialize", serde(skip))] - pub files: Option>, + // #[cfg_attr(feature = "serialize", serde(skip))] + // pub files: Option>, } impl Debug for FormData { diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index ab2e49f1e..577899fe1 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -22,6 +22,12 @@ mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; +#[cfg(feature = "serialize")] +mod transit; + +#[cfg(feature = "serialize")] +pub use transit::*; + pub use elements::*; pub use events::*; pub use global_attributes::*; diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs new file mode 100644 index 000000000..5e944e0ce --- /dev/null +++ b/packages/html/src/transit.rs @@ -0,0 +1,152 @@ +use std::{any::Any, rc::Rc}; + +use crate::events::*; +use dioxus_core::ElementId; +use serde::{Deserialize, Serialize}; + +// macro_rules! match_data { +// ( +// $m:ident; +// $name:ident; +// $( +// $tip:ty => $($mname:literal)|* ; +// )* +// ) => { +// match $name { +// $( $($mname)|* => { +// let val: $tip = from_value::<$tip>($m).ok()?; +// Rc::new(val) as Rc +// })* +// _ => return None, +// } +// }; +// } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HtmlEvent { + pub element: ElementId, + pub name: String, + pub data: EventData, + pub bubbles: bool, +} + +impl HtmlEvent { + pub fn bubbles(&self) -> bool { + event_bubbles(&self.name) + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(untagged)] +pub enum EventData { + Mouse(MouseData), + Clipboard(ClipboardData), + Composition(CompositionData), + Keyboard(KeyboardData), + Focus(FocusData), + Form(FormData), + Drag(DragData), + Pointer(PointerData), + Selection(SelectionData), + Touch(TouchData), + Scroll(ScrollData), + Wheel(WheelData), + Media(MediaData), + Animation(AnimationData), + Transition(TransitionData), + Toggle(ToggleData), +} + +impl EventData { + pub fn into_any(self) -> Rc { + match self { + EventData::Mouse(data) => Rc::new(data) as Rc, + EventData::Clipboard(data) => Rc::new(data) as Rc, + EventData::Composition(data) => Rc::new(data) as Rc, + EventData::Keyboard(data) => Rc::new(data) as Rc, + EventData::Focus(data) => Rc::new(data) as Rc, + EventData::Form(data) => Rc::new(data) as Rc, + EventData::Drag(data) => Rc::new(data) as Rc, + EventData::Pointer(data) => Rc::new(data) as Rc, + EventData::Selection(data) => Rc::new(data) as Rc, + EventData::Touch(data) => Rc::new(data) as Rc, + EventData::Scroll(data) => Rc::new(data) as Rc, + EventData::Wheel(data) => Rc::new(data) as Rc, + EventData::Media(data) => Rc::new(data) as Rc, + EventData::Animation(data) => Rc::new(data) as Rc, + EventData::Transition(data) => Rc::new(data) as Rc, + EventData::Toggle(data) => Rc::new(data) as Rc, + } + } +} + +#[test] +fn test_back_and_forth() { + let data = HtmlEvent { + element: ElementId(0), + data: EventData::Mouse(MouseData::default()), + name: "click".to_string(), + bubbles: true, + }; + + println!("{}", serde_json::to_string_pretty(&data).unwrap()); + + let o = r#" +{ + "element": 0, + "name": "click", + "bubbles": true, + "data": { + "alt_key": false, + "button": 0, + "buttons": 0, + "client_x": 0, + "client_y": 0, + "ctrl_key": false, + "meta_key": false, + "offset_x": 0, + "offset_y": 0, + "page_x": 0, + "page_y": 0, + "screen_x": 0, + "screen_y": 0, + "shift_key": false + } +} + "#; + + let p: HtmlEvent = serde_json::from_str(o).unwrap(); +} + +// pub fn decode_event(value: ) -> Option> { +// let val = value.data; +// let name = value.event.as_str(); +// type DragData = MouseData; + +// let evt = match_data! { val; name; +// MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup"; +// ClipboardData => "copy" | "cut" | "paste"; +// CompositionData => "compositionend" | "compositionstart" | "compositionupdate"; +// KeyboardData => "keydown" | "keypress" | "keyup"; +// FocusData => "blur" | "focus" | "focusin" | "focusout"; +// FormData => "change" | "input" | "invalid" | "reset" | "submit"; +// DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop"; +// PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture"; +// SelectionData => "selectstart" | "selectionchange" | "select"; +// TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart"; +// ScrollData => "scroll"; +// WheelData => "wheel"; +// MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" +// | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata" +// | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" +// | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" +// | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout"; +// AnimationData => "animationstart" | "animationend" | "animationiteration"; +// TransitionData => "transitionend"; +// ToggleData => "toggle"; +// // ImageData => "load" | "error"; +// // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; +// }; + +// Some(evt) +// } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 4eb7410bd..6fcf19f27 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { version = "0.2.79", optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } - +sledgehammer_bindgen = { version = "0.1.3", optional = true } +sledgehammer_utils = { version = "0.1.0", optional = true } [features] default = [] web = ["wasm-bindgen", "js-sys", "web-sys"] +sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index d028b8d30..478b8ca5c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -17,9 +17,6 @@ extern "C" { #[wasm_bindgen(method)] pub fn MountToRoot(this: &Interpreter); - #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, m: u32, id: u32); - #[wasm_bindgen(method)] pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); @@ -76,4 +73,7 @@ extern "C" { #[wasm_bindgen(method)] pub fn PushRoot(this: &Interpreter, id: u32); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, id: u32, m: u32); } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 93da98b0c..ada1bed64 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,11 +1,3 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")); - } -} - class ListenerMap { constructor(root) { // bubbling events can listen at the root element @@ -60,7 +52,7 @@ class ListenerMap { } } -export class Interpreter { +class Interpreter { constructor(root) { this.root = root; this.listeners = new ListenerMap(root); @@ -90,7 +82,8 @@ export class Interpreter { this.stack.pop(); } AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; + // let root = this.nodes[id]; + let root = this.stack[this.stack.length - 1 - many]; let to_add = this.stack.splice(this.stack.length - many); for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); @@ -352,7 +345,10 @@ export class Interpreter { break; case "NewEventListener": // this handler is only provided on desktop implementations since this - // method is not used by the web implementation + // method is not used by the web implementationa + + let bubbles = event_bubbles(edit.name); + let handler = (event) => { let target = event.target; if (target != null) { @@ -434,20 +430,21 @@ export class Interpreter { } window.ipc.postMessage( serializeIpcMessage("user_event", { - event: edit.name, - mounted_dom_id: parseInt(realId), - contents: contents, + name: edit.name, + element: parseInt(realId), + data: contents, + bubbles: bubbles, }) ); } }; - this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler); + this.NewEventListener(edit.name, edit.id, bubbles, handler); break; } } } -export function serialize_event(event) { +function serialize_event(event) { switch (event.type) { case "copy": case "cut": diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 076a89197..962bd6bbb 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,5 +1,10 @@ pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); +#[cfg(feature = "sledgehammer")] +mod sledgehammer_bindings; +#[cfg(feature = "sledgehammer")] +pub use sledgehammer_bindings::*; + #[cfg(feature = "web")] mod bindings; diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs new file mode 100644 index 000000000..a81e16235 --- /dev/null +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -0,0 +1,231 @@ +use js_sys::Function; +use sledgehammer_bindgen::bindgen; +use web_sys::Node; + +#[bindgen] +mod js { + const JS: &str = r#" + class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = null; + this.handler = null; + } + + create(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + + remove(element, event_name, bubbles) { + if (bubbles) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + } + function SetAttributeInner(node, field, value, ns) { + const name = field; + if (ns === "style") { + // ????? why do we need to do this + if (node.style === undefined) { + node.style = {}; + } + node.style[name] = value; + } else if (ns !== null && ns !== undefined && ns !== "") { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value !== node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value === "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + function LoadChild(ptr, len) { + // iterate through each number and get that child + node = stack[stack.length - 1]; + ptr_end = ptr + len; + for (; ptr < ptr_end; ptr++) { + end = m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + const listeners = new ListenerMap(); + let nodes = []; + let stack = []; + const templates = {}; + let node, els, end, ptr_end, k; + export function save_template(nodes, tmpl_id) { + templates[tmpl_id] = nodes; + } + export function set_node(id, node) { + nodes[id] = node; + } + export function initilize(root, handler) { + listeners.handler = handler; + nodes = [root]; + stack = [root]; + listeners.root = root; + } + function AppendChildren(id, many){ + root = nodes[id]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } + "#; + + extern "C" { + #[wasm_bindgen] + pub fn save_template(nodes: Vec, tmpl_id: u32); + + #[wasm_bindgen] + pub fn set_node(id: u32, node: Node); + + #[wasm_bindgen] + pub fn initilize(root: Node, handler: &Function); + } + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u32) { + "{AppendChildren($id$, $many$);}" + } + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u32) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u32) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u32) { + "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + } + fn remove(id: u32) { + "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + } + fn create_raw_text(text: &str) { + "{stack.push(document.createTextNode($text$));}" + } + fn create_text_node(text: &str, id: u32) { + "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { + r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"# + } + fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { + "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + } + fn set_text(id: u32, text: &str) { + "{nodes[$id$].textContent = $text$;}" + } + fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { + "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{name = $field$; + node = this.nodes[$id$]; + if (ns == "style") { + node.style.removeProperty(name); + } else if (ns !== null && ns !== undefined && ns !== "") { + node.removeAttributeNS(ns, name); + } else if (name === "value") { + node.value = ""; + } else if (name === "checked") { + node.checked = false; + } else if (name === "selected") { + node.selected = false; + } else if (name === "dangerous_inner_html") { + node.innerHTML = ""; + } else { + node.removeAttribute(name); + }}"# + } + fn assign_id(ptr: u32, len: u8, id: u32) { + "{nodes[$id$] = LoadChild($ptr$, $len$);}" + } + fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { + r#"{ + node = LoadChild($ptr$, $len$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + nodes[$id$] = node; + }"# + } + fn replace_placeholder(ptr: u32, len: u8, n: u32) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u32, index: u32, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 90f956289..ee75f71de 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus" license = "MIT/Apache-2.0" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" } - +dioxus-interpreter-js = { path = "../interpreter" } # warp warp = { version = "0.3", optional = true } @@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true } # salvo salvo = { version = "0.32.0", optional = true, features = ["ws"] } +thiserror = "1.0.37" +uuid = { version = "1.2.2", features = ["v4"] } +anyhow = "1.0.66" [dev-dependencies] tokio = { version = "1", features = ["full"] } @@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] } tower = "0.4.12" [features] -default = [] \ No newline at end of file +default = ["salvo"] + +[[example]] +name = "axum" +required-features = ["axum"] + +[[example]] +name = "salvo" +required-features = ["salvo"] + +[[example]] +name = "warp" +required-features = ["warp"] diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 450af30ef..410c0e9a4 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -1,32 +1,53 @@ -#[cfg(not(feature = "axum"))] -fn main() {} +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello axum! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "axum")] #[tokio::main] async fn main() { - use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; - use dioxus_core::{Element, LazyNodes, Scope}; pretty_env_logger::init(); - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); - let view = dioxus_liveview::new(addr); - let body = view.body("Dioxus Liveview"); + let view = dioxus_liveview::LiveViewPool::new(); let app = Router::new() - .route("/", get(move || async { Html(body) })) .route( - "/app", + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + .route( + "/ws", get(move |ws: WebSocketUpgrade| async move { ws.on_upgrade(move |socket| async move { - view.upgrade_axum(socket, app).await; + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; }) }), ); + + println!("Listening on http://{}", addr); + axum::Server::bind(&addr.to_string().parse().unwrap()) .serve(app.into_make_service()) .await diff --git a/packages/liveview/examples/salvo.rs b/packages/liveview/examples/salvo.rs index 132a62633..4afd8ff7e 100644 --- a/packages/liveview/examples/salvo.rs +++ b/packages/liveview/examples/salvo.rs @@ -1,55 +1,71 @@ -#[cfg(not(feature = "salvo"))] -fn main() {} +use dioxus::prelude::*; + +use dioxus_liveview::LiveViewPool; +use salvo::extra::affix; +use salvo::extra::ws::WsHandler; +use salvo::prelude::*; +use std::net::SocketAddr; +use std::sync::Arc; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello salvo! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "salvo")] #[tokio::main] async fn main() { - use std::sync::Arc; - - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use dioxus_liveview::Liveview; - use salvo::extra::affix; - use salvo::extra::ws::WsHandler; - use salvo::prelude::*; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = LiveViewPool::new(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); let router = Router::new() .hoop(affix::inject(Arc::new(view))) .get(index) - .push(Router::with_path("app").get(connect)); + .push(Router::with_path("ws").get(connect)); + + println!("Listening on http://{}", addr); + Server::new(TcpListener::bind(addr)).serve(router).await; +} - #[handler] - fn index(depot: &mut Depot, res: &mut Response) { - let view = depot.obtain::>().unwrap(); - let body = view.body("Dioxus LiveView"); - res.render(Text::Html(body)); - } +#[handler] +fn index(_depot: &mut Depot, res: &mut Response) { + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + res.render(Text::Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + ))); +} - #[handler] - async fn connect( - req: &mut Request, - depot: &mut Depot, - res: &mut Response, - ) -> Result<(), StatusError> { - let view = depot.obtain::>().unwrap().clone(); - let fut = WsHandler::new().handle(req, res)?; - let fut = async move { - if let Some(ws) = fut.await { - view.upgrade_salvo(ws, app).await; - } - }; - tokio::task::spawn(fut); - Ok(()) - } +#[handler] +async fn connect( + req: &mut Request, + depot: &mut Depot, + res: &mut Response, +) -> Result<(), StatusError> { + let view = depot.obtain::>().unwrap().clone(); + let fut = WsHandler::new().handle(req, res)?; + + tokio::spawn(async move { + if let Some(ws) = fut.await { + _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await; + } + }); + + Ok(()) } diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 7b2ddfef5..7e759ea90 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -1,35 +1,56 @@ -#[cfg(not(feature = "warp"))] -fn main() {} +use dioxus::prelude::*; +use dioxus_liveview::adapters::warp_adapter::warp_socket; +use dioxus_liveview::LiveViewPool; +use std::net::SocketAddr; +use warp::ws::Ws; +use warp::Filter; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello warp! {num}" + button { + onclick: move |_| num += 1, + "Increment" + } + } + }) +} -#[cfg(feature = "warp")] #[tokio::main] async fn main() { - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use warp::ws::Ws; - use warp::Filter; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body("Dioxus LiveView"); + let index = warp::path::end().map(move || { + warp::reply::html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/")) + )) + }); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade_warp(socket, app).await; - }) - })); - warp::serve(routes).run(addr).await; + let pool = LiveViewPool::new(); + + let ws = warp::path("ws") + .and(warp::ws()) + .and(warp::any().map(move || pool.clone())) + .map(move |ws: Ws, pool: LiveViewPool| { + ws.on_upgrade(|ws| async move { + let _ = pool.launch(warp_socket(ws), app).await; + }) + }); + + println!("Listening on http://{}", addr); + + warp::serve(index.or(ws)).run(addr).await; } diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index c60b267ea..646cde38e 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1,94 +1,23 @@ -use crate::events; +use crate::{LiveViewError, LiveViewSocket}; use axum::extract::ws::{Message, WebSocket}; -use dioxus_core::prelude::*; -use futures_util::{ - future::{select, Either}, - pin_mut, SinkExt, StreamExt, -}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use futures_util::{SinkExt, StreamExt}; -impl crate::Liveview { - pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - - pub async fn upgrade_axum_with_props( - &self, - ws: WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - socket: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - let (mut user_ws_tx, mut user_ws_rx) = socket.split(); - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - let vdom_fut = pool.clone().spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - let edits = vdom.rebuild(); - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - loop { - let new_event = { - let vdom_fut = vdom.wait_for_work(); - pin_mut!(vdom_fut); - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - loop { - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::Text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - vdom_fut.abort(); +fn transform_rx(message: Result) -> Result { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::Text(message)) } diff --git a/packages/liveview/src/adapters/salvo_adapter.rs b/packages/liveview/src/adapters/salvo_adapter.rs index 8a28784b8..19cb528c7 100644 --- a/packages/liveview/src/adapters/salvo_adapter.rs +++ b/packages/liveview/src/adapters/salvo_adapter.rs @@ -1,110 +1,25 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; +use futures_util::{SinkExt, StreamExt}; use salvo::extra::ws::{Message, WebSocket}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; -impl crate::Liveview { - pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_salvo_with_props( - &self, - ws: salvo::extra::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +use crate::{LiveViewError, LiveViewSocket}; + +/// Convert a salvo websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); + Ok(msg) +} - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 398c7cdd9..e5c821ce3 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -1,110 +1,28 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use crate::{LiveViewError, LiveViewSocket}; +use futures_util::{SinkExt, StreamExt}; use warp::ws::{Message, WebSocket}; -impl crate::Liveview { - pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_warp_with_props( - &self, - ws: warp::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = message + .map_err(|_| LiveViewError::SendingFailed)? + .into_bytes(); - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); + Ok(msg) +} - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs deleted file mode 100644 index 2c5f97cc7..000000000 --- a/packages/liveview/src/events.rs +++ /dev/null @@ -1,207 +0,0 @@ -#![allow(dead_code)] - -//! Convert a serialized event to an event trigger - -use std::any::Any; -use std::sync::Arc; - -use dioxus_core::ElementId; -// use dioxus_html::event_bubbles; -use dioxus_html::events::*; - -#[derive(serde::Serialize, serde::Deserialize)] -pub(crate) struct IpcMessage { - pub method: String, - pub params: serde_json::Value, -} - -pub(crate) fn parse_ipc_message(payload: &str) -> Option { - match serde_json::from_str(payload) { - Ok(message) => Some(message), - Err(_) => None, - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct ImEvent { - event: String, - mounted_dom_id: ElementId, - contents: serde_json::Value, -} - -pub fn trigger_from_serialized(_val: serde_json::Value) { - todo!() - // let ImEvent { - // event, - // mounted_dom_id, - // contents, - // } = serde_json::from_value(val).unwrap(); - - // let mounted_dom_id = Some(mounted_dom_id); - - // let name = event_name_from_type(&event); - // let event = make_synthetic_event(&event, contents); - - // UserEvent { - // name, - // scope_id: None, - // element: mounted_dom_id, - // data: event, - // bubbles: event_bubbles(name), - // } -} - -fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { - match name { - "copy" | "cut" | "paste" => { - // - Arc::new(ClipboardData {}) - } - "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); - Arc::new(evt) - } - "focus" | "blur" | "focusout" | "focusin" => { - // - Arc::new(FocusData {}) - } - - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "select" => { - // - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "scroll" => Arc::new(()), - - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), - - "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), - - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => { - // - Arc::new(MediaData {}) - } - - "toggle" => Arc::new(ToggleData {}), - - _ => Arc::new(()), - } -} - -fn event_name_from_type(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - _ => { - panic!("unsupported event type") - } - } -} diff --git a/packages/liveview/src/index.html b/packages/liveview/src/index.html deleted file mode 100644 index 292a1ae6a..000000000 --- a/packages/liveview/src/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - Dioxus app - - - -
- - - diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js deleted file mode 100644 index 7e99745f2..000000000 --- a/packages/liveview/src/interpreter.js +++ /dev/null @@ -1,973 +0,0 @@ -function main() { - let root = window.document.getElementById("main"); - - if (root != null) { - // create a new ipc - window.ipc = new IPC(root); - - window.ipc.send(serializeIpcMessage("initialize")); - } -} - -class IPC { - constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root); - - this.ws = new WebSocket(WS_ADDR); - - this.ws.onopen = () => { - console.log("Connected to the websocket"); - }; - - this.ws.onerror = (err) => { - console.error("Error: ", err); - }; - - this.ws.onmessage = (event) => { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); - }; - } - - send(msg) { - this.ws.send(msg); - } -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.lastNode = root; - this.listeners = new ListenerMap(root); - this.handlers = {}; - this.nodes = [root]; - this.parents = []; - } - checkAppendParent() { - if (this.parents.length > 0) { - const lastParent = this.parents[this.parents.length - 1]; - lastParent[1]--; - if (lastParent[1] === 0) { - this.parents.pop(); - } - lastParent[0].appendChild(this.lastNode); - } - } - AppendChildren(root, children) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - for (let i = 0; i < children.length; i++) { - node.appendChild(this.nodes[children[i]]); - } - } - ReplaceWith(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.replaceWith(...els); - } - InsertAfter(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.after(...els); - } - InsertBefore(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.before(...els); - } - Remove(root) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - this.lastNode = document.createTextNode(text); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - CreateElement(tag, root, children) { - this.lastNode = document.createElement(tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreateElementNs(tag, root, ns, children) { - this.lastNode = document.createElementNS(ns, tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreatePlaceholder(root) { - this.lastNode = document.createElement("pre"); - this.lastNode.hidden = true; - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - NewEventListener(event_name, root, handler, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, node, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.removeAttribute(`data-dioxus-id`); - this.listeners.remove(node, event_name, bubbles); - } - SetText(root, text) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.data = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns === "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value === "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, field, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns == "style") { - node.style.removeProperty(name); - } else if (ns !== null || ns !== undefined) { - node.removeAttributeNS(ns, name); - } else if (name === "value") { - node.value = ""; - } else if (name === "checked") { - node.checked = false; - } else if (name === "selected") { - node.selected = false; - } else if (name === "dangerous_inner_html") { - node.innerHTML = ""; - } else { - node.removeAttribute(name); - } - } - CloneNode(old, new_id) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - this.nodes[new_id] = node.cloneNode(true); - } - CloneNodeChildren(old, new_ids) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - const old_node = node.cloneNode(true); - let i = 0; - for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) { - this.nodes[new_ids[i++]] = node; - } - } - FirstChild() { - this.lastNode = this.lastNode.firstChild; - } - NextSibling() { - this.lastNode = this.lastNode.nextSibling; - } - ParentNode() { - this.lastNode = this.lastNode.parentNode; - } - StoreWithId(id) { - this.nodes[id] = this.lastNode; - } - SetLastNode(root) { - this.lastNode = this.nodes[root]; - } - handleEdits(edits) { - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.root, edit.children); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.nodes); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.nodes); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.nodes); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root, edit.children); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (shouldPreventDefault !== `onclick`) { - if (target.tagName === "A") { - event.preventDefault(); - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - for (let x = 0; x < target.elements.length; x++) { - let element = target.elements[x]; - let name = element.getAttribute("name"); - if (name != null) { - if (element.getAttribute("type") === "checkbox") { - // @ts-ignore - contents.values[name] = element.checked ? "true" : "false"; - } else if (element.getAttribute("type") === "radio") { - if (element.checked) { - contents.values[name] = element.value; - } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; - } - } - } - } - - if (realId === null) { - return; - } - realId = parseInt(realId); - window.ipc.send( - serializeIpcMessage("user_event", { - event: edit.event_name, - mounted_dom_id: realId, - contents: contents, - }) - ); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); - - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name, edit.ns); - break; - case "CloneNode": - this.CloneNode(edit.id, edit.new_id); - break; - case "CloneNodeChildren": - this.CloneNodeChildren(edit.id, edit.new_ids); - break; - case "FirstChild": - this.FirstChild(); - break; - case "NextSibling": - this.NextSibling(); - break; - case "ParentNode": - this.ParentNode(); - break; - case "StoreWithId": - this.StoreWithId(BigInt(edit.id)); - break; - case "SetLastNode": - this.SetLastNode(BigInt(edit.id)); - break; - } - } -} - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -function serializeIpcMessage(method, params = {}) { - return JSON.stringify({ method, params }); -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - return false; - case "loadedmetadata": - return false; - case "loadstart": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - } -} \ No newline at end of file diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 540d29974..ffddf0272 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -1,56 +1,55 @@ -#![allow(dead_code)] - -pub(crate) mod events; pub mod adapters { #[cfg(feature = "warp")] pub mod warp_adapter; + #[cfg(feature = "warp")] + pub use warp_adapter::*; #[cfg(feature = "axum")] pub mod axum_adapter; + #[cfg(feature = "axum")] + pub use axum_adapter::*; #[cfg(feature = "salvo")] pub mod salvo_adapter; + + #[cfg(feature = "salvo")] + pub use salvo_adapter::*; } -use std::net::SocketAddr; +pub use adapters::*; -use tokio_util::task::LocalPoolHandle; +pub mod pool; +use futures_util::{SinkExt, StreamExt}; +pub use pool::*; -#[derive(Clone)] -pub struct Liveview { - pool: LocalPoolHandle, - addr: String, +pub trait WebsocketTx: SinkExt {} +impl WebsocketTx for T where T: SinkExt {} + +pub trait WebsocketRx: StreamExt> {} +impl WebsocketRx for T where T: StreamExt> {} + +#[derive(Debug, thiserror::Error)] +pub enum LiveViewError { + #[error("warp error")] + SendingFailed, } -impl Liveview { - pub fn body(&self, header: &str) -> String { - format!( - r#" - - - - {header} - - -
- - -"#, - addr = self.addr, - interpreter = include_str!("../src/interpreter.js") - ) - } -} +use dioxus_interpreter_js::INTERPRETER_JS; +static MAIN_JS: &str = include_str!("./main.js"); -pub fn new(addr: impl Into) -> Liveview { - let addr: SocketAddr = addr.into(); - - Liveview { - pool: LocalPoolHandle::new(16), - addr: addr.to_string(), - } +/// This script that gets injected into your app connects this page to the websocket endpoint +/// +/// Once the endpoint is connected, it will send the initial state of the app, and then start +/// processing user events and returning edits to the liveview instance +pub fn interpreter_glue(url: &str) -> String { + format!( + r#" + + "# + ) } diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js new file mode 100644 index 000000000..e37ac7b67 --- /dev/null +++ b/packages/liveview/src/main.js @@ -0,0 +1,36 @@ +function main() { + let root = window.document.getElementById("main"); + + if (root != null) { + // create a new ipc + window.ipc = new IPC(root); + window.ipc.postMessage(serializeIpcMessage("initialize")); + } +} + +class IPC { + constructor(root) { + // connect to the websocket + window.interpreter = new Interpreter(root); + + this.ws = new WebSocket(WS_ADDR); + + this.ws.onopen = () => { + console.log("Connected to the websocket"); + }; + + this.ws.onerror = (err) => { + console.error("Error: ", err); + }; + + this.ws.onmessage = (event) => { + console.log("Received message: ", event.data); + let edits = JSON.parse(event.data); + window.interpreter.handleEdits(edits); + }; + } + + postMessage(msg) { + this.ws.send(msg); + } +} diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs new file mode 100644 index 000000000..c769e1371 --- /dev/null +++ b/packages/liveview/src/pool.rs @@ -0,0 +1,123 @@ +use crate::LiveViewError; +use dioxus_core::prelude::*; +use dioxus_html::HtmlEvent; +use futures_util::{pin_mut, SinkExt, StreamExt}; +use std::time::Duration; +use tokio_util::task::LocalPoolHandle; + +#[derive(Clone)] +pub struct LiveViewPool { + pub(crate) pool: LocalPoolHandle, +} + +impl Default for LiveViewPool { + fn default() -> Self { + Self::new() + } +} + +impl LiveViewPool { + pub fn new() -> Self { + LiveViewPool { + pool: LocalPoolHandle::new(16), + } + } + + pub async fn launch( + &self, + ws: impl LiveViewSocket, + app: fn(Scope<()>) -> Element, + ) -> Result<(), LiveViewError> { + self.launch_with_props(ws, app, ()).await + } + + pub async fn launch_with_props( + &self, + ws: impl LiveViewSocket, + app: fn(Scope) -> Element, + props: T, + ) -> Result<(), LiveViewError> { + match self.pool.spawn_pinned(move || run(app, props, ws)).await { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => Err(e), + Err(_) => Err(LiveViewError::SendingFailed), + } + } +} + +/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client +pub trait LiveViewSocket: + SinkExt + + StreamExt> + + Send + + 'static +{ +} + +impl LiveViewSocket for S where + S: SinkExt + + StreamExt> + + Send + + 'static +{ +} + +/// The primary event loop for the VirtualDom waiting for user input +/// +/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework. +/// +/// As long as your framework can provide a Sink and Stream of Strings, you can use this function. +/// +/// You might need to transform the error types of the web backend into the LiveView error type. +pub async fn run( + app: Component, + props: T, + ws: impl LiveViewSocket, +) -> Result<(), LiveViewError> +where + T: Send + 'static, +{ + let mut vdom = VirtualDom::new_with_props(app, props); + + // todo: use an efficient binary packed format for this + let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + + // pin the futures so we can use select! + pin_mut!(ws); + + // send the initial render to the client + ws.send(edits).await?; + + // desktop uses this wrapper struct thing around the actual event itself + // this is sorta driven by tao/wry + #[derive(serde::Deserialize)] + struct IpcMessage { + params: HtmlEvent, + } + + loop { + tokio::select! { + // poll any futures or suspense + _ = vdom.wait_for_work() => {} + + evt = ws.next() => { + match evt { + Some(Ok(evt)) => { + if let Ok(IpcMessage { params }) = serde_json::from_str::(&evt) { + vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + } + } + // log this I guess? when would we get an error here? + Some(Err(_e)) => {}, + None => return Ok(()), + } + } + } + + let edits = vdom + .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) + .await; + + ws.send(serde_json::to_string(&edits).unwrap()).await?; + } +} diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index dda96b512..5bf50afcc 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -306,8 +306,7 @@ impl Member { + field.ty.to_token_stream().to_string().as_str()) .as_str(), Span::call_site(), - ) - .into(), + ), ident: field.ident.as_ref()?.clone(), }) } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 7d56b0dca..621e16577 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -102,11 +102,7 @@ impl RealDom { self.tree.add_child(node_id, child_id); } - fn create_template_node( - &mut self, - node: &TemplateNode, - mutations_vec: &mut FxHashMap, - ) -> RealNodeId { + fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId { match node { TemplateNode::Element { tag, @@ -139,27 +135,18 @@ impl RealDom { }); let node_id = self.create_node(node); for child in *children { - let child_id = self.create_template_node(child, mutations_vec); + let child_id = self.create_template_node(child); self.add_child(node_id, child_id); } node_id } - TemplateNode::Text { text } => { - let node_id = self.create_node(Node::new(NodeType::Text { - text: text.to_string(), - })); - node_id - } - TemplateNode::Dynamic { .. } => { - let node_id = self.create_node(Node::new(NodeType::Placeholder)); - node_id - } - TemplateNode::DynamicText { .. } => { - let node_id = self.create_node(Node::new(NodeType::Text { - text: String::new(), - })); - node_id - } + TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text { + text: text.to_string(), + })), + TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)), + TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text { + text: String::new(), + })), } } @@ -172,7 +159,7 @@ impl RealDom { for template in mutations.templates { let mut template_root_ids = Vec::new(); for root in template.roots { - let id = self.create_template_node(root, &mut nodes_updated); + let id = self.create_template_node(root); template_root_ids.push(id); } self.templates @@ -319,7 +306,7 @@ impl RealDom { } mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated); } - NewEventListener { name, scope: _, id } => { + NewEventListener { name, id } => { let node_id = self.element_to_node_id(id); let node = self.tree.get_mut(node_id).unwrap(); if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type { diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs new file mode 100644 index 000000000..86bcb71b1 --- /dev/null +++ b/packages/native-core/src/utils/cursor.rs @@ -0,0 +1,484 @@ +use std::cmp::Ordering; + +use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pos { + pub col: usize, + pub row: usize, +} + +impl Pos { + pub fn new(col: usize, row: usize) -> Self { + Self { row, col } + } + + pub fn up(&mut self, rope: &str) { + self.move_row(-1, rope); + } + + pub fn down(&mut self, rope: &str) { + self.move_row(1, rope); + } + + pub fn right(&mut self, rope: &str) { + self.move_col(1, rope); + } + + pub fn left(&mut self, rope: &str) { + self.move_col(-1, rope); + } + + pub fn move_row(&mut self, change: i32, rope: &str) { + let new = self.row as i32 + change; + if new >= 0 && new < rope.lines().count() as i32 { + self.row = new as usize; + } + } + + pub fn move_col(&mut self, change: i32, rope: &str) { + self.realize_col(rope); + let idx = self.idx(rope) as i32; + if idx + change >= 0 && idx + change <= rope.len() as i32 { + let len_line = self.len_line(rope) as i32; + let new_col = self.col as i32 + change; + let diff = new_col - len_line; + if diff > 0 { + self.down(rope); + self.col = 0; + self.move_col(diff - 1, rope); + } else if new_col < 0 { + self.up(rope); + self.col = self.len_line(rope); + self.move_col(new_col + 1, rope); + } else { + self.col = new_col as usize; + } + } + } + + pub fn col(&self, rope: &str) -> usize { + self.col.min(self.len_line(rope)) + } + + pub fn row(&self) -> usize { + self.row + } + + fn len_line(&self, rope: &str) -> usize { + let line = rope.lines().nth(self.row).unwrap_or_default(); + let len = line.len(); + if len > 0 && line.chars().nth(len - 1) == Some('\n') { + len - 1 + } else { + len + } + } + + pub fn idx(&self, rope: &str) -> usize { + rope.lines().take(self.row).map(|l| l.len()).sum::() + self.col(rope) + } + + // the column can be more than the line length, cap it + pub fn realize_col(&mut self, rope: &str) { + self.col = self.col(rope); + } +} + +impl Ord for Pos { + fn cmp(&self, other: &Self) -> Ordering { + self.row.cmp(&other.row).then(self.col.cmp(&other.col)) + } +} + +impl PartialOrd for Pos { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cursor { + pub start: Pos, + pub end: Option, +} + +impl Cursor { + pub fn from_start(pos: Pos) -> Self { + Self { + start: pos, + end: None, + } + } + + pub fn new(start: Pos, end: Pos) -> Self { + Self { + start, + end: Some(end), + } + } + + fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) { + if shift { + self.with_end(f); + } else { + f(&mut self.start); + self.end = None; + } + } + + fn delete_selection(&mut self, text: &mut String) -> [i32; 2] { + let first = self.first(); + let last = self.last(); + let dr = first.row as i32 - last.row as i32; + let dc = if dr != 0 { + -(last.col as i32) + } else { + first.col as i32 - last.col as i32 + }; + text.replace_range(first.idx(text)..last.idx(text), ""); + if let Some(end) = self.end.take() { + if self.start > end { + self.start = end; + } + } + [dc, dr] + } + + pub fn handle_input( + &mut self, + data: &dioxus_html::KeyboardData, + text: &mut String, + max_width: usize, + ) { + use Code::*; + match data.code() { + ArrowUp => { + self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT)); + } + ArrowDown => { + self.move_cursor( + |c| c.down(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + ArrowRight => { + if data.modifiers().contains(Modifiers::CONTROL) { + self.move_cursor( + |c| { + let mut change = 1; + let idx = c.idx(text); + let length = text.len(); + while idx + change < length { + let chr = text.chars().nth(idx + change).unwrap(); + if chr.is_whitespace() { + break; + } + change += 1; + } + c.move_col(change as i32, text); + }, + data.modifiers().contains(Modifiers::SHIFT), + ); + } else { + self.move_cursor( + |c| c.right(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + } + ArrowLeft => { + if data.modifiers().contains(Modifiers::CONTROL) { + self.move_cursor( + |c| { + let mut change = -1; + let idx = c.idx(text) as i32; + while idx + change > 0 { + let chr = text.chars().nth((idx + change) as usize).unwrap(); + if chr == ' ' { + break; + } + change -= 1; + } + c.move_col(change as i32, text); + }, + data.modifiers().contains(Modifiers::SHIFT), + ); + } else { + self.move_cursor( + |c| c.left(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + } + End => { + self.move_cursor( + |c| c.col = c.len_line(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + Home => { + self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT)); + } + Backspace => { + self.start.realize_col(text); + let mut start_idx = self.start.idx(text); + if self.end.is_some() { + self.delete_selection(text); + } else if start_idx > 0 { + self.start.left(text); + text.replace_range(start_idx - 1..start_idx, ""); + if data.modifiers().contains(Modifiers::CONTROL) { + start_idx = self.start.idx(text); + while start_idx > 0 + && text + .chars() + .nth(start_idx - 1) + .filter(|c| *c != ' ') + .is_some() + { + self.start.left(text); + text.replace_range(start_idx - 1..start_idx, ""); + start_idx = self.start.idx(text); + } + } + } + } + Enter => { + if text.len() + 1 - self.selection_len(text) <= max_width { + text.insert(self.start.idx(text), '\n'); + self.start.col = 0; + self.start.down(text); + } + } + Tab => { + if text.len() + 1 - self.selection_len(text) <= max_width { + self.start.realize_col(text); + self.delete_selection(text); + text.insert(self.start.idx(text), '\t'); + self.start.right(text); + } + } + _ => { + self.start.realize_col(text); + if let Key::Character(character) = data.key() { + if text.len() + 1 - self.selection_len(text) <= max_width { + self.delete_selection(text); + let character = character.chars().next().unwrap(); + text.insert(self.start.idx(text), character); + self.start.right(text); + } + } + } + } + } + + pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) { + let mut new = self.end.take().unwrap_or_else(|| self.start.clone()); + f(&mut new); + self.end.replace(new); + } + + pub fn first(&self) -> &Pos { + if let Some(e) = &self.end { + e.min(&self.start) + } else { + &self.start + } + } + + pub fn last(&self) -> &Pos { + if let Some(e) = &self.end { + e.max(&self.start) + } else { + &self.start + } + } + + pub fn selection_len(&self, text: &str) -> usize { + self.last().idx(text) - self.first().idx(text) + } +} + +impl Default for Cursor { + fn default() -> Self { + Self { + start: Pos::new(0, 0), + end: None, + } + } +} + +#[test] +fn pos_direction_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + pos.down(text); + assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len()); + pos.up(text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + pos.left(text); + assert_eq!( + pos.col(text), + text.lines().next().unwrap_or_default().len() - 1 + ); + pos.right(text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); +} + +#[test] +fn pos_col_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + // move inside a row + pos.move_col(-5, text); + assert_eq!( + pos.col(text), + text.lines().next().unwrap_or_default().len() - 5 + ); + pos.move_col(5, text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + + // move between rows + pos.move_col(3, text); + assert_eq!(pos.col(text), 2); + pos.move_col(-3, text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + + // don't panic if moving out of range + pos.move_col(-100, text); + pos.move_col(1000, text); +} + +#[test] +fn cursor_row_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + pos.move_row(1, text); + assert_eq!(pos.row(), 1); + pos.move_row(-1, text); + assert_eq!(pos.row(), 0); + + // don't panic if moving out of range + pos.move_row(-100, text); + pos.move_row(1000, text); +} + +#[test] +fn cursor_input() { + let mut cursor = Cursor::from_start(Pos::new(0, 0)); + let mut text = "hello world\nhi".to_string(); + + for _ in 0..5 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::ArrowRight, + dioxus_html::input_data::keyboard_types::Code::ArrowRight, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + 10, + ); + } + + for _ in 0..5 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Backspace, + dioxus_html::input_data::keyboard_types::Code::Backspace, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + 10, + ); + } + + assert_eq!(text, " world\nhi"); + + let goal_text = "hello world\nhi"; + let max_width = goal_text.len(); + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyH, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyE, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyL, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyL, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyO, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + // these should be ignored + for _ in 0..10 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyO, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + } + + assert_eq!(text.to_string(), goal_text); +} diff --git a/packages/native-core/src/utils/mod.rs b/packages/native-core/src/utils/mod.rs new file mode 100644 index 000000000..58e98b0fa --- /dev/null +++ b/packages/native-core/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod persistant_iterator; +pub use persistant_iterator::*; +pub mod cursor; diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils/persistant_iterator.rs similarity index 100% rename from packages/native-core/src/utils.rs rename to packages/native-core/src/utils/persistant_iterator.rs diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 0e7cf0927..5b72e8117 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -36,6 +36,7 @@ thiserror = "1.0.30" futures-util = "0.3.21" serde = { version = "1", optional = true } serde_urlencoded = { version = "0.7.1", optional = true } +simple_logger = "4.0.0" [features] default = ["query"] @@ -50,6 +51,9 @@ wasm-logger = "0.2.0" wasm-bindgen-test = "0.3" gloo-utils = "0.1.2" dioxus-web = { path = "../web" } +# dioxus-desktop = { path = "../desktop", optional = true } + +# not wasm [target.wasm32-unknown-unknown.dev-dependencies] dioxus-router = { path = ".", features = ["web"] } diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 4c16c29a1..9b94c7a9d 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element { Link { to: "/blog", li { "blog" } } Link { to: "/blog/tim", li { "tims' blog" } } Link { to: "/blog/bill", li { "bills' blog" } } + Link { to: "/blog/james", li { "james amazing' blog" } } Link { to: "/apples", li { "go to apples" } } } Route { to: "/", Home {} } @@ -42,5 +43,10 @@ fn BlogPost(cx: Scope) -> Element { log::trace!("rendering blog post {}", id); - cx.render(rsx! { div { "{id:?}" } }) + cx.render(rsx! { + div { + h3 { "blog post: {id:?}" } + Link { to: "/blog/", "back to blog list" } + } + }) } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 5098090b2..d077966eb 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -120,9 +120,17 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { prevent_default: "{prevent_default}", target: format_args!("{}", if * new_tab { "_blank" } else { "" }), onclick: move |_| { + log::trace!("Clicked link to {}", to); + if !outerlink { if let Some(service) = svc { + log::trace!("Pushing route to {}", to); service.push_route(to, cx.props.title.map(|f| f.to_string()), None); + + #[cfg(feature = "web")] + { + web_sys::window().unwrap().scroll_to_with_x_and_y(0.0, 0.0); + } } else { log::error!( "Attempted to create a Link to {} outside of a Router context", cx.props diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 7bfc48993..cf665d2e1 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { router_root.register_total_route(route_context.total_route, cx.scope_id()); }); - log::trace!("Checking Route: {:?}", cx.props.to); + log::debug!("Checking Route: {:?}", cx.props.to); if router_root.should_render(cx.scope_id()) { - log::trace!("Route should render: {:?}", cx.scope_id()); + log::debug!("Route should render: {:?}", cx.scope_id()); cx.render(rsx!(&cx.props.children)) } else { - log::trace!("Route should *not* render: {:?}", cx.scope_id()); + log::debug!("Route should *not* render: {:?}", cx.scope_id()); cx.render(rsx!(())) } } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 10e2d7d33..e089ca1be 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -180,6 +180,7 @@ impl RouterService { (self.regen_any_route)(self.router_id); for listener in self.onchange_listeners.borrow().iter() { + log::trace!("Regenerating scope {:?}", listener); (self.regen_any_route)(*listener); } diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 40655ed77..62eb13823 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "dioxus-rsx" -version = "0.0.0" +version = "0.0.1" edition = "2018" license = "MIT/Apache-2.0" +description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus-rsx" +keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,6 +15,5 @@ license = "MIT/Apache-2.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } syn = { version = "1.0", features = ["full", "extra-traits"] } quote = { version = "1.0" } -dioxus-core = { path = "../core", features = ["serialize"] } serde = { version = "1.0", features = ["derive"] } internment = "0.7.0" \ No newline at end of file diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 307059f9d..ae4bfabe9 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -45,7 +45,7 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) && content.peek2(Token![,]) { + if content.peek(LitStr) { let value = content.parse()?; attributes.push(ElementAttrNamed { el_name: el_name.clone(), @@ -53,7 +53,6 @@ impl Parse for Element { }); } else { let value = content.parse::()?; - attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index c0488a6da..521c15490 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -217,7 +217,8 @@ impl<'a, Ctx: HotReloadingContext> ToTokens for TemplateRenderer<'a, Ctx> { parent: None, key: #key_tokens, template: TEMPLATE, - root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([::dioxus::core::ElementId(0); #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(), + root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut _).as_slice_of_cells(), + // root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(), dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), } @@ -441,7 +442,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> { let el_name = &el.name; let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { ElementAttr::AttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); + let value = value.to_static().unwrap(); quote! { ::dioxus::core::TemplateAttribute::Static { name: dioxus_elements::#el_name::#name.0, @@ -455,7 +456,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> { } ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); + let value = value.to_static().unwrap(); quote! { ::dioxus::core::TemplateAttribute::Static { name: #name, @@ -503,7 +504,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> { } BodyNode::Text(text) if text.is_static() => { - let text = text.source.as_ref().unwrap(); + let text = text.to_static().unwrap(); quote! { ::dioxus::core::TemplateNode::Text{ text: #text } } } diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index 116784c7b..94aaf3296 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dioxus = { path = "../dioxus", version = "^0.2.1" } dioxus-core = { path = "../core", version = "^0.2.1" } dioxus-html = { path = "../html", version = "^0.2.1" } dioxus-native-core = { path = "../native-core", version = "^0.2.0" } diff --git a/packages/tui/examples/tui_colorpicker.rs b/packages/tui/examples/tui_colorpicker.rs index edbcbdddc..951c4d1e5 100644 --- a/packages/tui/examples/tui_colorpicker.rs +++ b/packages/tui/examples/tui_colorpicker.rs @@ -17,12 +17,14 @@ fn app(cx: Scope) -> Element { width: "100%", background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { - if let RenderReturn::Sync(Ok(node))=cx.root_node(){ - let node = tui_query.get(node.root_ids[0].get()); - let Size{width, height} = node.size().unwrap(); - let pos = evt.inner().element_coordinates(); - hue.set((pos.x as f32/width as f32)*255.0); - brightness.set((pos.y as f32/height as f32)*100.0); + if let RenderReturn::Sync(Ok(node)) = cx.root_node() { + if let Some(id) = node.root_ids[0].get() { + let node = tui_query.get(id); + let Size{width, height} = node.size().unwrap(); + let pos = evt.inner().element_coordinates(); + hue.set((pos.x as f32/width as f32)*255.0); + brightness.set((pos.y as f32/height as f32)*100.0); + } } }, "hsl({hue}, 70%, {brightness}%)", diff --git a/packages/tui/examples/tui_widgets.rs b/packages/tui/examples/tui_widgets.rs new file mode 100644 index 000000000..793057bc5 --- /dev/null +++ b/packages/tui/examples/tui_widgets.rs @@ -0,0 +1,93 @@ +use dioxus::prelude::*; +use dioxus_html::FormData; +use dioxus_tui::prelude::*; +use dioxus_tui::Config; + +fn main() { + dioxus_tui::launch_cfg(app, Config::new()); +} + +fn app(cx: Scope) -> Element { + let bg_green = use_state(cx, || false); + + let color = if *bg_green.get() { "green" } else { "red" }; + cx.render(rsx! { + div{ + width: "100%", + background_color: "{color}", + flex_direction: "column", + align_items: "center", + justify_content: "center", + + Input{ + oninput: |data: FormData| if &data.value == "good"{ + bg_green.set(true); + } else{ + bg_green.set(false); + }, + r#type: "checkbox", + value: "good", + width: "50%", + height: "10%", + checked: "true", + } + Input{ + oninput: |data: FormData| if &data.value == "hello world"{ + bg_green.set(true); + } else{ + bg_green.set(false); + }, + width: "50%", + height: "10%", + maxlength: "11", + } + Input{ + oninput: |data: FormData| { + if (data.value.parse::().unwrap() - 40.0).abs() < 5.0 { + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "range", + width: "50%", + height: "10%", + min: "20", + max: "80", + } + Input{ + oninput: |data: FormData| { + if data.value == "10"{ + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "number", + width: "50%", + height: "10%", + maxlength: "4", + } + Input{ + oninput: |data: FormData| { + if data.value == "hello world"{ + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "password", + width: "50%", + height: "10%", + maxlength: "11", + } + Input{ + onclick: |_: FormData| bg_green.set(true), + r#type: "button", + value: "green", + width: "50%", + height: "10%", + } + } + }) +} diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index b57b71e13..d627ac3e0 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -287,7 +287,10 @@ impl InnerInputState { fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData { let Point { x, y } = layout.location; - let node_origin = ClientPoint::new(x.into(), y.into()); + let node_origin = ClientPoint::new( + layout_to_screen_space(x).into(), + layout_to_screen_space(y).into(), + ); let new_client_coordinates = (mouse_data.client_coordinates() - node_origin) .to_point() diff --git a/packages/tui/src/layout.rs b/packages/tui/src/layout.rs index d863f13d8..ba50b59a6 100644 --- a/packages/tui/src/layout.rs +++ b/packages/tui/src/layout.rs @@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState; use dioxus_native_core_macro::sorted_str_slice; use taffy::prelude::*; -use crate::screen_to_layout_space; +use crate::{screen_to_layout_space, unit_to_layout_space}; #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum PossiblyUninitalized { @@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout { child_layout.push(l.node.unwrap()); } + fn scale_dimention(d: Dimension) -> Dimension { + match d { + Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)), + Dimension::Percent(p) => Dimension::Percent(p), + Dimension::Auto => Dimension::Auto, + Dimension::Undefined => Dimension::Undefined, + } + } + let style = Style { + position: Rect { + left: scale_dimention(style.position.left), + right: scale_dimention(style.position.right), + top: scale_dimention(style.position.top), + bottom: scale_dimention(style.position.bottom), + }, + margin: Rect { + left: scale_dimention(style.margin.left), + right: scale_dimention(style.margin.right), + top: scale_dimention(style.margin.top), + bottom: scale_dimention(style.margin.bottom), + }, + padding: Rect { + left: scale_dimention(style.padding.left), + right: scale_dimention(style.padding.right), + top: scale_dimention(style.padding.top), + bottom: scale_dimention(style.padding.bottom), + }, + border: Rect { + left: scale_dimention(style.border.left), + right: scale_dimention(style.border.right), + top: scale_dimention(style.border.top), + bottom: scale_dimention(style.border.bottom), + }, + gap: Size { + width: scale_dimention(style.gap.width), + height: scale_dimention(style.gap.height), + }, + flex_basis: scale_dimention(style.flex_basis), + size: Size { + width: scale_dimention(style.size.width), + height: scale_dimention(style.size.height), + }, + min_size: Size { + width: scale_dimention(style.min_size.width), + height: scale_dimention(style.min_size.height), + }, + max_size: Size { + width: scale_dimention(style.max_size.width), + height: scale_dimention(style.max_size.height), + }, + ..style + }; + if let PossiblyUninitalized::Initialized(n) = self.node { if self.style != style { taffy.set_style(n, style).unwrap(); diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 38638f7c5..7b4f72ce3 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -1,5 +1,6 @@ use anyhow::Result; use crossterm::{ + cursor::{MoveTo, RestorePosition, SavePosition, Show}, event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -28,11 +29,13 @@ mod focus; mod hooks; mod layout; mod node; +pub mod prelude; pub mod query; mod render; mod style; mod style_attributes; mod widget; +mod widgets; pub use config::*; pub use hooks::*; @@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 { screen as f32 * 10.0 } +pub(crate) fn unit_to_layout_space(screen: f32) -> f32 { + screen * 10.0 +} + pub(crate) fn layout_to_screen_space(layout: f32) -> f32 { layout / 10.0 } @@ -136,7 +143,13 @@ fn render_vdom( let mut terminal = (!cfg.headless).then(|| { enable_raw_mode().unwrap(); let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + MoveTo(0, 1000) + ) + .unwrap(); let backend = CrosstermBackend::new(io::stdout()); Terminal::new(backend).unwrap() }); @@ -181,14 +194,16 @@ fn render_vdom( taffy.compute_layout(root_node, size).unwrap(); } if let Some(terminal) = &mut terminal { + execute!(terminal.backend_mut(), SavePosition).unwrap(); terminal.draw(|frame| { let rdom = rdom.borrow(); let mut taffy = taffy.lock().expect("taffy lock poisoned"); // size is guaranteed to not change when rendering - resize(frame.size(), &mut *taffy, &rdom); + resize(frame.size(), &mut taffy, &rdom); let root = &rdom[NodeId(0)]; - render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO); + render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO); })?; + execute!(terminal.backend_mut(), RestorePosition, Show).unwrap(); } else { let rdom = rdom.borrow(); resize( diff --git a/packages/tui/src/prelude/mod.rs b/packages/tui/src/prelude/mod.rs new file mode 100644 index 000000000..2155f7707 --- /dev/null +++ b/packages/tui/src/prelude/mod.rs @@ -0,0 +1 @@ +pub use crate::widgets::*; diff --git a/packages/tui/src/query.rs b/packages/tui/src/query.rs index 64da87650..c052ad9ea 100644 --- a/packages/tui/src/query.rs +++ b/packages/tui/src/query.rs @@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> { .ok(); layout.map(|layout| Layout { order: layout.order, - size: layout.size.map(|v| layout_to_screen_space(v)), + size: layout.size.map(layout_to_screen_space), location: Point { x: layout_to_screen_space(layout.location.x), y: layout_to_screen_space(layout.location.y), diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs index ef18f2989..e7de5eaac 100644 --- a/packages/tui/src/render.rs +++ b/packages/tui/src/render.rs @@ -41,7 +41,7 @@ pub(crate) fn render_vnode( let x = layout_to_screen_space(fx).round() as u16; let y = layout_to_screen_space(fy).round() as u16; let Size { width, height } = *size; - let width = layout_to_screen_space(fx + width).round() as u16 + x; + let width = layout_to_screen_space(fx + width).round() as u16 - x; let height = layout_to_screen_space(fy + height).round() as u16 - y; match &node.node_data.node_type { diff --git a/packages/tui/src/widgets/button.rs b/packages/tui/src/widgets/button.rs new file mode 100644 index 000000000..845a60c08 --- /dev/null +++ b/packages/tui/src/widgets/button.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct ButtonProps<'a> { + #[props(!optional)] + raw_onclick: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { + let state = use_state(cx, || false); + let width = cx.props.width.unwrap_or("1px"); + let height = cx.props.height.unwrap_or("1px"); + + let single_char = width == "1px" || height == "1px"; + let text = if let Some(v) = cx.props.value { v } else { "" }; + let border_style = if single_char { "none" } else { "solid" }; + let update = || { + let new_state = !state.get(); + if let Some(callback) = cx.props.raw_onclick { + callback.call(FormData { + value: text.to_string(), + values: HashMap::new(), + }); + } + state.set(new_state); + }; + cx.render(rsx! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border_style}", + flex_direction: "row", + align_items: "center", + justify_content: "center", + onclick: move |_| { + update(); + }, + onkeydown: move |evt|{ + if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { + update(); + } + }, + "{text}" + } + }) +} diff --git a/packages/tui/src/widgets/checkbox.rs b/packages/tui/src/widgets/checkbox.rs new file mode 100644 index 000000000..90c7212e9 --- /dev/null +++ b/packages/tui/src/widgets/checkbox.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct CheckBoxProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, + #[props(!optional)] + checked: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> { + let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some()); + let width = cx.props.width.unwrap_or("1px"); + let height = cx.props.height.unwrap_or("1px"); + + let single_char = width == "1px" && height == "1px"; + let text = if single_char { + if *state.get() { + "☑" + } else { + "☐" + } + } else if *state.get() { + "✓" + } else { + " " + }; + let border_style = if width == "1px" || height == "1px" { + "none" + } else { + "solid" + }; + let update = move || { + let new_state = !state.get(); + if let Some(callback) = cx.props.raw_oninput { + callback.call(FormData { + value: if let Some(value) = &cx.props.value { + if new_state { + value.to_string() + } else { + String::new() + } + } else { + "on".to_string() + }, + values: HashMap::new(), + }); + } + state.set(new_state); + }; + cx.render(rsx! { + div { + width: "{width}", + height: "{height}", + border_style: "{border_style}", + align_items: "center", + justify_content: "center", + onclick: move |_| { + update(); + }, + onkeydown: move |evt| { + if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { + update(); + } + }, + "{text}" + } + }) +} diff --git a/packages/tui/src/widgets/input.rs b/packages/tui/src/widgets/input.rs new file mode 100644 index 000000000..2294b0f10 --- /dev/null +++ b/packages/tui/src/widgets/input.rs @@ -0,0 +1,102 @@ +use dioxus::prelude::*; +use dioxus_core::prelude::fc_to_builder; +use dioxus_html::FormData; + +use crate::widgets::button::Button; +use crate::widgets::checkbox::CheckBox; +use crate::widgets::number::NumbericInput; +use crate::widgets::password::Password; +use crate::widgets::slider::Slider; +use crate::widgets::textbox::TextBox; + +#[derive(Props)] +pub struct InputProps<'a> { + r#type: Option<&'static str>, + oninput: Option>, + onclick: Option>, + value: Option<&'a str>, + size: Option<&'a str>, + maxlength: Option<&'a str>, + width: Option<&'a str>, + height: Option<&'a str>, + min: Option<&'a str>, + max: Option<&'a str>, + step: Option<&'a str>, + checked: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> { + cx.render(match cx.props.r#type { + Some("checkbox") => { + rsx! { + CheckBox{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + checked: cx.props.checked, + } + } + } + Some("range") => { + rsx! { + Slider{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + max: cx.props.max, + min: cx.props.min, + step: cx.props.step, + } + } + } + Some("button") => { + rsx! { + Button{ + raw_onclick: cx.props.onclick.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + } + } + } + Some("number") => { + rsx! { + NumbericInput{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + Some("password") => { + rsx! { + Password{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + _ => { + rsx! { + TextBox{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + }) +} diff --git a/packages/tui/src/widgets/mod.rs b/packages/tui/src/widgets/mod.rs new file mode 100644 index 000000000..25201bddb --- /dev/null +++ b/packages/tui/src/widgets/mod.rs @@ -0,0 +1,18 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod textbox; + +use dioxus_core::{ElementId, RenderReturn, Scope}; +pub use input::*; + +pub(crate) fn get_root_id(cx: Scope) -> Option { + if let RenderReturn::Sync(Ok(sync)) = cx.root_node() { + sync.root_ids.get(0).and_then(|id| id.get()) + } else { + None + } +} diff --git a/packages/tui/src/widgets/number.rs b/packages/tui/src/widgets/number.rs new file mode 100644 index 000000000..93f9edd94 --- /dev/null +++ b/packages/tui/src/widgets/number.rs @@ -0,0 +1,208 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::MoveTo, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct NumbericInputProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + let update = |text: String| { + if let Some(input_handler) = &cx.props.raw_oninput { + input_handler.call(FormData { + value: text, + values: HashMap::new(), + }); + } + }; + let increase = move || { + let mut text = text_ref.write(); + *text = (text.parse::().unwrap_or(0.0) + 1.0).to_string(); + update(text.clone()); + }; + let decrease = move || { + let mut text = text_ref.write(); + *text = (text.parse::().unwrap_or(0.0) - 1.0).to_string(); + update(text.clone()); + }; + + render! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + let is_text = match k.key(){ + Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true, + Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true, + _ => false, + }; + if is_text{ + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + update(text.clone()); + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + match k.key() { + Key::ArrowUp =>{ + increase(); + } + Key::ArrowDown =>{ + decrease(); + } + _ => () + } + } + }, + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} diff --git a/packages/tui/src/widgets/password.rs b/packages/tui/src/widgets/password.rs new file mode 100644 index 000000000..c881404b3 --- /dev/null +++ b/packages/tui/src/widgets/password.rs @@ -0,0 +1,185 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::*, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct PasswordProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let text_before_first_cursor = ".".repeat(text_before_first_cursor.len()); + let text_highlighted = ".".repeat(text_highlighted.len()); + let text_after_second_cursor = ".".repeat(text_after_second_cursor.len()); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + render! { + div { + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + if k.key()== Key::Enter { + return; + } + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + if let Some(input_handler) = &cx.props.raw_oninput{ + input_handler.call(FormData{ + value: text.clone(), + values: HashMap::new(), + }); + } + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} diff --git a/packages/tui/src/widgets/slider.rs b/packages/tui/src/widgets/slider.rs new file mode 100644 index 000000000..257c765d0 --- /dev/null +++ b/packages/tui/src/widgets/slider.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use crate::widgets::get_root_id; +use crate::Query; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct SliderProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, + #[props(!optional)] + min: Option<&'a str>, + #[props(!optional)] + max: Option<&'a str>, + #[props(!optional)] + step: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + + let value_state = use_state(cx, || 0.0); + let value: Option = cx.props.value.and_then(|v| v.parse().ok()); + let width = cx.props.width.unwrap_or("20px"); + let height = cx.props.height.unwrap_or("1px"); + let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0); + let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0); + let size = max - min; + let step = cx + .props + .step + .and_then(|v| v.parse().ok()) + .unwrap_or(size / 10.0); + + let current_value = if let Some(value) = value { + value + } else { + *value_state.get() + } + .max(min) + .min(max); + let fst_width = 100.0 * (current_value - min) / size; + let snd_width = 100.0 * (max - current_value) / size; + assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0); + + let update = |value: String| { + if let Some(oninput) = cx.props.raw_oninput { + oninput.call(FormData { + value, + values: HashMap::new(), + }); + } + }; + + render! { + div{ + width: "{width}", + height: "{height}", + display: "flex", + flex_direction: "row", + onkeydown: move |event| { + match event.key() { + Key::ArrowLeft => { + value_state.set((current_value - step).max(min).min(max)); + update(value_state.current().to_string()); + } + Key::ArrowRight => { + value_state.set((current_value + step).max(min).min(max)); + update(value_state.current().to_string()); + } + _ => () + } + }, + onmousemove: move |evt| { + let mouse = evt.data; + if !mouse.held_buttons().is_empty(){ + let node = tui_query.get(get_root_id(cx).unwrap()); + let width = node.size().unwrap().width; + let offset = mouse.element_coordinates(); + value_state.set(min + size*(offset.x as f32) / width as f32); + update(value_state.current().to_string()); + } + }, + div{ + width: "{fst_width}%", + background_color: "rgba(10,10,10,0.5)", + } + div{ + "|" + } + div{ + width: "{snd_width}%", + background_color: "rgba(10,10,10,0.5)", + } + } + } +} diff --git a/packages/tui/src/widgets/textbox.rs b/packages/tui/src/widgets/textbox.rs new file mode 100644 index 000000000..ce0ffcc99 --- /dev/null +++ b/packages/tui/src/widgets/textbox.rs @@ -0,0 +1,181 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::*, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct TextBoxProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + render! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + if k.key() == Key::Enter { + return; + } + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + if let Some(input_handler) = &cx.props.raw_oninput{ + input_handler.call(FormData{ + value: text.clone(), + values: HashMap::new(), + }); + } + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 94e1e8613..40056eaaa 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] } dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [ - "web" + "sledgehammer" ] } js-sys = "0.3.56" @@ -30,6 +30,7 @@ futures-util = "0.3.19" smallstr = "0.2.0" futures-channel = "0.3.21" serde_json = { version = "1.0" } +serde = { version = "1.0" } serde-wasm-bindgen = "0.4.5" [dependencies.web-sys] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 0cdf46b3e..5037c4d27 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,24 +7,34 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{Mutation, Template}; +use dioxus_core::{ElementId, Mutation, Template, TemplateAttribute, TemplateNode}; use dioxus_html::{event_bubbles, CompositionData, FormData}; -use dioxus_interpreter_js::Interpreter; +use dioxus_interpreter_js::{save_template, Channel}; use futures_channel::mpsc; +use rustc_hash::FxHashMap; use std::{any::Any, rc::Rc}; use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{Document, Element, Event}; +use web_sys::{Document, Element, Event, HtmlElement}; use crate::Config; pub struct WebsysDom { - interpreter: Interpreter, - handler: Closure, - _root: Element, + document: Document, + templates: FxHashMap, + max_template_id: u32, + interpreter: Channel, +} + +pub struct UiEvent { + pub name: String, + pub bubbles: bool, + pub element: ElementId, + pub data: Rc, + pub event: Event, } impl WebsysDom { - pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { + pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type // a match here in order to avoid some error during runtime browser test let document = load_document(); @@ -32,67 +42,158 @@ impl WebsysDom { Some(root) => root, None => document.create_element("body").ok().unwrap(), }; + let interpreter = Channel::default(); + let handler: Closure = + Closure::wrap(Box::new(move |event: &web_sys::Event| { + let name = event.type_(); + let element = walk_event_for_id(event); + let bubbles = dioxus_html::event_bubbles(name.as_str()); + if let Some((element, target)) = element { + if target + .get_attribute("dioxus-prevent-default") + .as_deref() + .map(|f| f.trim_start_matches("on")) + == Some(&name) + { + event.prevent_default(); + } + + let data = virtual_event_from_websys_event(event.clone(), target); + let _ = event_channel.unbounded_send(UiEvent { + name, + bubbles, + element, + data, + event: event.clone(), + }); + } + })); + + dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref()); + handler.forget(); Self { - interpreter: Interpreter::new(root.clone()), - _root: root, - handler: Closure::wrap(Box::new(move |event: &web_sys::Event| { - let _ = event_channel.unbounded_send(event.clone()); - })), + document, + interpreter, + templates: FxHashMap::default(), + max_template_id: 0, } } pub fn mount(&mut self) { - self.interpreter.MountToRoot(); + self.interpreter.mount_to_root(); } pub fn load_templates(&mut self, templates: &[Template]) { - log::debug!("Loading templates {:?}", templates); - for template in templates { - self.interpreter - .SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap()); + let mut roots = vec![]; + + for root in template.roots { + roots.push(self.create_template_node(root)) + } + + self.templates + .insert(template.name.to_owned(), self.max_template_id); + save_template(roots, self.max_template_id); + self.max_template_id += 1 + } + } + + fn create_template_node(&self, v: &TemplateNode) -> web_sys::Node { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + let el = match namespace { + Some(ns) => self.document.create_element_ns(Some(ns), tag).unwrap(), + None => self.document.create_element(tag).unwrap(), + }; + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + match namespace { + Some(ns) if *ns == "style" => el + .dyn_ref::() + .unwrap() + .style() + .set_property(name, value) + .unwrap(), + Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(), + None => el.set_attribute(name, value).unwrap(), + } + } + } + for child in *children { + let _ = el.append_child(&self.create_template_node(child)); + } + el.dyn_into().unwrap() + } + Text { text } => self.document.create_text_node(text).dyn_into().unwrap(), + DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(), + Dynamic { .. } => { + let el = self.document.create_element("pre").unwrap(); + let _ = el.toggle_attribute("hidden"); + el.dyn_into().unwrap() + } } } pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; - let i = &self.interpreter; - for edit in edits.drain(..) { + let i = &mut self.interpreter; + for edit in &edits { match edit { - AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32), - AssignId { path, id } => i.AssignId(path, id.0 as u32), - CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32), - CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32), - HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32), - LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32), - ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32), - ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32), - InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32), - InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32), + AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), + AssignId { path, id } => { + i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32) + } + CreatePlaceholder { id } => i.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => { + i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32) + } + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = self.templates.get(*name) { + i.load_template(*tmpl_id, *index as u32, id.0 as u32) + } + } + ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32), + ReplacePlaceholder { path, m } => { + i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32) + } + InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32), + InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32), SetAttribute { name, value, id, ns, - } => i.SetAttribute(id.0 as u32, name, value.into(), ns), + } => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()), SetBoolAttribute { name, value, id } => { - i.SetBoolAttribute(id.0 as u32, name, value) + i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "") } - SetText { value, id } => i.SetText(id.0 as u32, value.into()), + SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - self.interpreter.NewEventListener( - name, - id.0 as u32, - event_bubbles(&name[2..]), - self.handler.as_ref().unchecked_ref(), - ); + i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8); } - RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32), - Remove { id } => i.Remove(id.0 as u32), - PushRoot { id } => i.PushRoot(id.0 as u32), + RemoveEventListener { name, id } => { + i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8) + } + Remove { id } => i.remove(id.0 as u32), + PushRoot { id } => i.push_root(id.0 as u32), } } + edits.clear(); + i.flush(); } } @@ -230,9 +331,28 @@ fn read_input_to_data(target: Element) -> Rc { } } - Rc::new(FormData { - value, - values, - files: None, - }) + Rc::new(FormData { value, values }) +} + +fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { + use wasm_bindgen::JsCast; + + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => return Some((ElementId(id), target)), + Some(Err(_)) => return None, + + // walk the tree upwards until we actually find an event target + None => match target.parent_element() { + Some(parent) => target = parent, + None => return None, + }, + } + } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e7acc43e9..9c66825c9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -55,7 +55,7 @@ pub use crate::cfg::Config; use crate::dom::virtual_event_from_websys_event; -pub use crate::util::use_eval; +pub use crate::util::{use_eval, EvalResult}; use dioxus_core::{Element, ElementId, Scope, VirtualDom}; use futures_util::{pin_mut, FutureExt, StreamExt}; @@ -195,7 +195,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // the mutations come back with nothing - we need to actually mount them websys_dom.mount(); - let mut work_loop = ric_raf::RafLoop::new(); + let _work_loop = ric_raf::RafLoop::new(); loop { log::debug!("waiting for work"); @@ -222,16 +222,11 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // Dequeue all of the events from the channel in send order // todo: we should re-order these if possible while let Some(evt) = res { - let name = evt.type_(); - let element = walk_event_for_id(&evt); - let bubbles = dioxus_html::event_bubbles(name.as_str()); - if let Some((element, target)) = element { - let data = virtual_event_from_websys_event(evt, target); - dom.handle_event(name.as_str(), data, element, bubbles); - } + dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles); res = rx.try_next().transpose().unwrap().ok(); } + // Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks // Jank free rendering // // 1. wait for the browser to give us "idle" time @@ -240,42 +235,19 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // 4. Wait for the animation frame to patch the dom // wait for the mainthread to schedule us in - let deadline = work_loop.wait_for_idle_time().await; + // let deadline = work_loop.wait_for_idle_time().await; // run the virtualdom work phase until the frame deadline is reached - let edits = dom.render_with_deadline(deadline).await; + let edits = dom.render_immediate(); // wait for the animation frame to fire so we can apply our changes - work_loop.wait_for_raf().await; + // work_loop.wait_for_raf().await; websys_dom.load_templates(&edits.templates); websys_dom.apply_edits(edits.edits); } } -fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { - use wasm_bindgen::JsCast; - - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => return Some((ElementId(id), target)), - Some(Err(_)) => return None, - - // walk the tree upwards until we actually find an event target - None => match target.parent_element() { - Some(parent) => target = parent, - None => return None, - }, - } - } -} - // if should_hydrate { // // todo: we need to split rebuild and initialize into two phases // // it's a waste to produce edits just to get the vdom loaded diff --git a/packages/web/src/util.rs b/packages/web/src/util.rs index 772f53608..3ed8bd479 100644 --- a/packages/web/src/util.rs +++ b/packages/web/src/util.rs @@ -1,6 +1,13 @@ //! Utilities specific to websys +use std::{ + future::{IntoFuture, Ready}, + str::FromStr, +}; + use dioxus_core::*; +use serde::de::Error; +use serde_json::Value; /// Get a closure that executes any JavaScript in the webpage. /// @@ -15,12 +22,51 @@ use dioxus_core::*; /// /// The closure will panic if the provided script is not valid JavaScript code /// or if it returns an uncaught error. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { cx.use_hook(|| { |script: S| { - js_sys::Function::new_no_args(&script.to_string()) - .call0(&wasm_bindgen::JsValue::NULL) - .expect("failed to eval script"); + let body = script.to_string(); + EvalResult { + value: if let Ok(value) = + js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL) + { + if let Ok(stringified) = js_sys::JSON::stringify(&value) { + if !stringified.is_undefined() && stringified.is_valid_utf16() { + let string: String = stringified.into(); + Value::from_str(&string) + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to execute script")) + }, + } } }) } + +/// A wrapper around the result of a JavaScript evaluation. +/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult. +pub struct EvalResult { + value: Result, +} + +impl EvalResult { + /// Get the result of the Javascript execution. + pub fn get(self) -> Result { + self.value + } +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Ready>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.value) + } +}