From b7516743541e35b2b3ac9b60b89e4c50cd65b7b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 11:16:34 -0800 Subject: [PATCH] Web works with the js structure --- Cargo.lock | 2 - packages/interpreter/Cargo.toml | 3 +- packages/interpreter/build.rs | 5 +- packages/interpreter/src/js/common.js | 99 ++++++++ .../interpreter/src/js/{web.js => core.js} | 163 +++++++------ packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 217 +++++++++++------- packages/interpreter/src/lib.rs | 9 +- packages/interpreter/src/ts/common.ts | 2 + .../src/ts/{interpreter_core.ts => core.ts} | 74 +++++- .../interpreter/src/ts/interpreter_web.ts | 79 ------- .../ts/{interpreter_native.ts => native.ts} | 4 +- packages/interpreter/src/unified_bindings.rs | 124 +++++----- packages/web/src/dom.rs | 4 +- packages/web/src/mutations.rs | 4 +- packages/web/src/rehydrate.rs | 4 +- 16 files changed, 470 insertions(+), 325 deletions(-) create mode 100644 packages/interpreter/src/js/common.js rename packages/interpreter/src/js/{web.js => core.js} (95%) create mode 100644 packages/interpreter/src/ts/common.ts rename packages/interpreter/src/ts/{interpreter_core.ts => core.ts} (57%) delete mode 100644 packages/interpreter/src/ts/interpreter_web.ts rename packages/interpreter/src/ts/{interpreter_native.ts => native.ts} (98%) diff --git a/Cargo.lock b/Cargo.lock index f037f0a10..ae75345b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8527,7 +8527,6 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen" version = "0.4.0" -source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "sledgehammer_bindgen_macro", "wasm-bindgen", @@ -8536,7 +8535,6 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen_macro" version = "0.4.0" -source = "git+https://github.com/ealmloff/sledgehammer_bindgen#6f90290e1655014f22623eb8b2ea41fb098e1164" dependencies = [ "quote", "syn 2.0.51", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 1cb573911..64edf911a 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,7 +17,8 @@ web-sys = { version = "0.3.56", optional = true, features = [ "Element", "Node", ] } -sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } +sledgehammer_bindgen = { path = "../../../Tinkering/sledgehammer_bindgen", default-features = false, optional = true } +# sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 75c070d80..90e7823a6 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -20,8 +20,9 @@ fn main() { // Otherwise, generate the bindings and write the new hash to disk // Generate the bindings for both native and web - gen_bindings("interpreter_native", "native"); - gen_bindings("interpreter_web", "web"); + gen_bindings("common", "common"); + gen_bindings("native", "native"); + gen_bindings("core", "core"); std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); } diff --git a/packages/interpreter/src/js/common.js b/packages/interpreter/src/js/common.js new file mode 100644 index 000000000..25d0ee691 --- /dev/null +++ b/packages/interpreter/src/js/common.js @@ -0,0 +1,99 @@ +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; +// src/ts/form.ts +function retrieveFormValues(form) { + const formData = new FormData(form); + const contents = {}; + formData.forEach((value, key) => { + if (contents[key]) { + contents[key] += "," + value; + } else { + contents[key] = value; + } + }); + return { + valid: form.checkValidity(), + values: contents + }; +} +export { + setAttributeInner, + retrieveFormValues +}; diff --git a/packages/interpreter/src/js/web.js b/packages/interpreter/src/js/core.js similarity index 95% rename from packages/interpreter/src/js/web.js rename to packages/interpreter/src/js/core.js index 1de671893..8487578c1 100644 --- a/packages/interpreter/src/js/web.js +++ b/packages/interpreter/src/js/core.js @@ -1,81 +1,3 @@ -// src/ts/interpreter_core.ts -class Interpreter { - global; - local; - root; - handler; - nodes; - stack; - templates; - constructor(root, handler) { - this.handler = handler; - this.initialize(root); - } - initialize(root, handler = null) { - this.global = {}; - this.local = {}; - this.root = root; - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - if (handler) { - this.handler = handler; - } - } - createListener(event_name, element, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - 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); - } - } - removeListener(element, event_name, bubbles) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - removeBubblingListener(event_name) { - 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]; - } - } - removeNonBubblingListener(element, event_name) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - removeAllNonBubblingListeners(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - getNode(id) { - return this.nodes[id]; - } - appendChildren(id, many) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0;k < many; k++) { - root.appendChild(els[k]); - } - } -} - // src/ts/set_attribute.ts function setAttributeInner(node, field, value, ns) { if (ns === "style") { @@ -156,13 +78,84 @@ var isBoolAttr = function(field) { } }; -// src/ts/interpreter_web.ts -class PlatformInterpreter extends Interpreter { +// src/ts/core.ts +class BaseInterpreter { + global; + local; + root; + handler; + nodes; + stack; + templates; m; constructor(root, handler) { - super(root, handler); + this.handler = handler; + this.initialize(root); } - LoadChild(ptr, len) { + initialize(root, handler = null) { + this.global = {}; + this.local = {}; + this.root = root; + this.nodes = [root]; + this.stack = [root]; + this.templates = {}; + if (handler) { + this.handler = handler; + } + } + createListener(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = { active: 1, callback: this.handler }; + 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); + } + } + removeListener(element, event_name, bubbles) { + if (bubbles) { + this.removeBubblingListener(event_name); + } else { + this.removeNonBubblingListener(element, event_name); + } + } + removeBubblingListener(event_name) { + 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]; + } + } + removeNonBubblingListener(element, event_name) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (Object.keys(this.local[id]).length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + removeAllNonBubblingListeners(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + getNode(id) { + return this.nodes[id]; + } + appendChildren(id, many) { + const root = this.nodes[id]; + const els = this.stack.splice(this.stack.length - many); + for (let k = 0;k < many; k++) { + root.appendChild(els[k]); + } + } + loadChild(ptr, len) { let node = this.stack[this.stack.length - 1]; let ptr_end = ptr + len; for (;ptr < ptr_end; ptr++) { @@ -207,8 +200,10 @@ class PlatformInterpreter extends Interpreter { currentNode = treeWalker.nextNode(); } } + setAttributeInner(node, field, value, ns) { + setAttributeInner(node, field, value, ns); + } } export { - setAttributeInner, - PlatformInterpreter + BaseInterpreter }; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index a6b01b52c..cba33cfe2 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -3599957386864841107 \ No newline at end of file +7906913262979346544 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 20ab039a9..012e5f1ce 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1,5 +1,85 @@ -// src/ts/interpreter_core.ts -class Interpreter { +// src/ts/set_attribute.ts +function setAttributeInner(node, field, value, ns) { + if (ns === "style") { + node.style.setProperty(field, value); + return; + } + if (!!ns) { + node.setAttributeNS(ns, field, value); + return; + } + switch (field) { + case "value": + if (node.value !== value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + if (!truthy(value) && isBoolAttr(field)) { + node.removeAttribute(field); + } else { + node.setAttribute(field, value); + } + } +} +var truthy = function(val) { + return val === "true" || val === true; +}; +var isBoolAttr = function(field) { + switch (field) { + case "allowfullscreen": + case "allowpaymentrequest": + case "async": + case "autofocus": + case "autoplay": + case "checked": + case "controls": + case "default": + case "defer": + case "disabled": + case "formnovalidate": + case "hidden": + case "ismap": + case "itemscope": + case "loop": + case "multiple": + case "muted": + case "nomodule": + case "novalidate": + case "open": + case "playsinline": + case "readonly": + case "required": + case "reversed": + case "selected": + case "truespeed": + case "webkitdirectory": + return true; + default: + return false; + } +}; + +// src/ts/core.ts +class BaseInterpreter { global; local; root; @@ -7,6 +87,7 @@ class Interpreter { nodes; stack; templates; + m; constructor(root, handler) { this.handler = handler; this.initialize(root); @@ -74,6 +155,54 @@ class Interpreter { root.appendChild(els[k]); } } + loadChild(ptr, len) { + let node = this.stack[this.stack.length - 1]; + let ptr_end = ptr + len; + for (;ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild;end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + saveTemplate(nodes, tmpl_id) { + this.templates[tmpl_id] = nodes; + } + hydrateRoot(ids) { + const hydrateNodes = document.querySelectorAll("[data-node-hydration]"); + for (let i = 0;i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i]; + const hydration = hydrateNode.getAttribute("data-node-hydration"); + const split = hydration.split(","); + const id = ids[parseInt(split[0])]; + this.nodes[id] = hydrateNode; + if (split.length > 1) { + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute("data-dioxus-id", id.toString()); + for (let j = 1;j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(":"); + const event_name = split2[0]; + const bubbles = split2[1] === "1"; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); + let currentNode = treeWalker.nextNode(); + while (currentNode) { + const id = currentNode.textContent; + const split = id.split("node-id"); + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + currentNode = treeWalker.nextNode(); + } + } + setAttributeInner(node, field, value, ns) { + setAttributeInner(node, field, value, ns); + } } // src/ts/form.ts @@ -291,87 +420,7 @@ var serializeDragEvent = function(event) { }; }; -// src/ts/set_attribute.ts -function setAttributeInner(node, field, value, ns) { - if (ns === "style") { - node.style.setProperty(field, value); - return; - } - if (!!ns) { - node.setAttributeNS(ns, field, value); - return; - } - switch (field) { - case "value": - if (node.value !== value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "initial_checked": - node.defaultChecked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - break; - case "initial_selected": - node.defaultSelected = truthy(value); - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - if (!truthy(value) && isBoolAttr(field)) { - node.removeAttribute(field); - } else { - node.setAttribute(field, value); - } - } -} -var truthy = function(val) { - return val === "true" || val === true; -}; -var isBoolAttr = function(field) { - switch (field) { - case "allowfullscreen": - case "allowpaymentrequest": - case "async": - case "autofocus": - case "autoplay": - case "checked": - case "controls": - case "default": - case "defer": - case "disabled": - case "formnovalidate": - case "hidden": - case "ismap": - case "itemscope": - case "loop": - case "multiple": - case "muted": - case "nomodule": - case "novalidate": - case "open": - case "playsinline": - case "readonly": - case "required": - case "reversed": - case "selected": - case "truespeed": - case "webkitdirectory": - return true; - default: - return false; - } -}; - -// src/ts/interpreter_native.ts +// src/ts/native.ts var targetId = function(target) { if (!(target instanceof Node)) { return null; @@ -390,7 +439,7 @@ var targetId = function(target) { return parseInt(realId); }; -class PlatformInterpreter extends Interpreter { +class PlatformInterpreter extends BaseInterpreter { intercept_link_redirects; ipc; liveview; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 1db7cda86..6d21f0b5b 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -2,8 +2,11 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -pub static INTERPRETER_JS: &str = include_str!("./js/native.js"); -pub static COMMON_JS: &str = include_str!("./js/web.js"); +/// The base class that the JS channel will extend +pub static INTERPRETER_JS: &str = include_str!("./js/core.js"); + +/// The code explicitly for desktop/liveview that bridges the eval gap between the two +pub static NATIVE_JS: &str = include_str!("./js/native.js"); #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))] mod write_native_mutations; @@ -25,7 +28,7 @@ pub mod minimal_bindings { /// Some useful snippets that we use to share common functionality between the different platforms we support. /// /// This maintains some sort of consistency between web, desktop, and liveview - #[wasm_bindgen(module = "/src/js/web.js")] + #[wasm_bindgen(module = "/src/js/common.js")] extern "C" { /// Set the attribute of the node pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); diff --git a/packages/interpreter/src/ts/common.ts b/packages/interpreter/src/ts/common.ts new file mode 100644 index 000000000..643f36436 --- /dev/null +++ b/packages/interpreter/src/ts/common.ts @@ -0,0 +1,2 @@ +export { setAttributeInner } from "./set_attribute"; +export { retrieveFormValues } from "./form"; diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/core.ts similarity index 57% rename from packages/interpreter/src/ts/interpreter_core.ts rename to packages/interpreter/src/ts/core.ts index 11b0c83f8..f5e1cd94a 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/core.ts @@ -1,9 +1,11 @@ // The root interpreter class that holds state about the mapping between DOM and VirtualDom // This always lives in the JS side of things, and is extended by the native and web interpreters +import { setAttributeInner } from "./set_attribute"; + export type NodeId = number; -export class Interpreter { +export class BaseInterpreter { // non bubbling events listen at the element the listener was created at global: { [key: string]: { active: number, callback: EventListener } @@ -23,6 +25,9 @@ export class Interpreter { [key: string]: Node[] }; + // sledgehammer is generating this... + m: any; + constructor(root: HTMLElement, handler: EventListener) { this.handler = handler; this.initialize(root); @@ -100,5 +105,72 @@ export class Interpreter { root.appendChild(els[k]); } } + + loadChild(ptr: number, len: number): Node { + // iterate through each number and get that child + let node = this.stack[this.stack.length - 1] as Node; + let ptr_end = ptr + len; + + for (; ptr < ptr_end; ptr++) { + let end = this.m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + + return node; + } + + saveTemplate(nodes: HTMLElement[], tmpl_id: string) { + this.templates[tmpl_id] = nodes; + } + + hydrateRoot(ids: { [key: number]: number }) { + const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + + for (let i = 0; i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i] as HTMLElement; + const hydration = hydrateNode.getAttribute('data-node-hydration'); + const split = hydration!.split(','); + const id = ids[parseInt(split[0])]; + + this.nodes[id] = hydrateNode; + + if (split.length > 1) { + // @ts-ignore + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id.toString()); + for (let j = 1; j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(':'); + const event_name = split2[0]; + const bubbles = split2[1] === '1'; + this.createListener(event_name, hydrateNode, bubbles); + } + } + } + + const treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ); + + let currentNode = treeWalker.nextNode(); + + while (currentNode) { + const id = currentNode.textContent!; + const split = id.split('node-id'); + + if (split.length > 1) { + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + + currentNode = treeWalker.nextNode(); + } + } + + setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { + setAttributeInner(node, field, value, ns); + } } diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts deleted file mode 100644 index 86f195446..000000000 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ /dev/null @@ -1,79 +0,0 @@ -// The JS<->Rust bridge -// This file is thin, suitable just for the web -// If you want the more full-featured intrepreter, look at the native interpreter which extends this with additional functionality -// -// We're using sledgehammer directly - -import { Interpreter } from "./interpreter_core"; -export { setAttributeInner } from "./set_attribute"; - -export class PlatformInterpreter extends Interpreter { - m: any; - - constructor(root: HTMLElement, handler: EventListener) { - super(root, handler); - } - - LoadChild(ptr: number, len: number): Node { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; - let ptr_end = ptr + len; - - for (; ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - saveTemplate(nodes: HTMLElement[], tmpl_id: string) { - this.templates[tmpl_id] = nodes; - } - - hydrateRoot(ids: { [key: number]: number }) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i] as HTMLElement; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration!.split(','); - const id = ids[parseInt(split[0])]; - - this.nodes[id] = hydrateNode; - - if (split.length > 1) { - // @ts-ignore - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id.toString()); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - - let currentNode = treeWalker.nextNode(); - - while (currentNode) { - const id = currentNode.textContent!; - const split = id.split('node-id'); - - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - - currentNode = treeWalker.nextNode(); - } - } -} diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/native.ts similarity index 98% rename from packages/interpreter/src/ts/interpreter_native.ts rename to packages/interpreter/src/ts/native.ts index 968bad631..ddce3239a 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -3,11 +3,11 @@ // This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't // provide since it doesn't have access to the dom. -import { Interpreter, NodeId } from "./interpreter_core"; +import { BaseInterpreter, NodeId } from "./core"; import { SerializedEvent, serializeEvent } from "./serialize"; import { setAttributeInner } from "./set_attribute"; -export class PlatformInterpreter extends Interpreter { +export class PlatformInterpreter extends BaseInterpreter { intercept_link_redirects: boolean; ipc: any; diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs index d1e3b2b0d..9ab7cfd00 100644 --- a/packages/interpreter/src/unified_bindings.rs +++ b/packages/interpreter/src/unified_bindings.rs @@ -1,32 +1,54 @@ #[cfg(feature = "webonly")] use web_sys::Node; -#[cfg(feature = "webonly")] -use wasm_bindgen::prelude::wasm_bindgen; - -use sledgehammer_bindgen::bindgen; - -/// Combine the interpreter class with the sledgehammer_bindgen generated methods. -pub fn native_js() -> String { - format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,) -} - pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; -#[bindgen(module)] +#[cfg(feature = "webonly")] +#[wasm_bindgen::prelude::wasm_bindgen] +extern "C" { + pub type BaseInterpreter; + + #[wasm_bindgen(method)] + pub fn initialize(this: &BaseInterpreter, root: Node, handler: &js_sys::Function); + + #[wasm_bindgen(method, js_name = "saveTemplate")] + pub fn save_template(this: &BaseInterpreter, nodes: Vec, tmpl_id: u16); + + #[wasm_bindgen(method)] + pub fn hydrate(this: &BaseInterpreter, ids: Vec); + + #[wasm_bindgen(method, js_name = "getNode")] + pub fn get_node(this: &BaseInterpreter, id: u32) -> Node; +} + +// Note that this impl is for the sledgehammer interpreter to allow us dropping down to the base interpreter +// During hydration and initialization we need to the base interpreter methods +#[cfg(feature = "webonly")] +impl Interpreter { + /// Convert the interpreter to its baseclass, giving + pub fn base(&self) -> &BaseInterpreter { + use wasm_bindgen::prelude::JsCast; + &self.js_channel().unchecked_ref() + } +} + +#[sledgehammer_bindgen::bindgen(module)] mod js { + // Extend the web base class + const BASE: &str = "./src/js/core.js"; + /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`. - #[extends(PlatformInterpreter)] + #[extends(BaseInterpreter)] pub struct Interpreter; fn mount_to_root() { - "{this.AppendChildren(this.root, this.stack.length-1);}" + "{this.appendChildren(this.root, this.stack.length-1);}" } fn push_root(root: u32) { "{this.stack.push(this.nodes[$root$]);}" } fn append_children(id: u32, many: u16) { - "{this.AppendChildren($id$, $many$);}" + "{this.appendChildren($id$, $many$);}" } fn pop_root() { "{this.stack.pop();}" @@ -93,11 +115,11 @@ mod js { }"# } fn assign_id(ptr: u32, len: u8, id: u32) { - "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}" + "{this.nodes[$id$] = this.loadChild($ptr$, $len$);}" } fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { r#"{ - let node = this.LoadChild($ptr$, $len$); + let node = this.loadChild($ptr$, $len$); if (node.nodeType == node.TEXT_NODE) { node.textContent = value; } else { @@ -109,20 +131,13 @@ mod js { }"# } fn replace_placeholder(ptr: u32, len: u8, n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}" + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($ptr$, $len$); node.replaceWith(...this.els);}" } fn load_template(tmpl_id: u16, index: u16, id: u32) { "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}" } - /* - Binary protocol methods only! - - These methods let us support binary packing mutations for use on boundaries like desktop where we prefer to send - binary data instead of JSON. - - We're using native types in a number of places - */ + #[cfg(feature = "binary-protocol")] fn append_children_to_top(many: u16) { "{ let root = this.stack[this.stack.length-many-1]; @@ -130,23 +145,35 @@ mod js { for (let k = 0; k < many; k++) { root.appendChild(this.els[k]); } - }" + }" } + + #[cfg(feature = "binary-protocol")] fn set_top_attribute(field: &str, value: &str, ns: &str) { "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" } + + #[cfg(feature = "binary-protocol")] fn add_placeholder() { "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}" } + + #[cfg(feature = "binary-protocol")] fn create_element(element: &'static str) { "{this.stack.push(document.createElement($element$))}" } + + #[cfg(feature = "binary-protocol")] fn create_element_ns(element: &'static str, ns: &'static str) { "{this.stack.push(document.createElementNS($ns$, $element$))}" } + + #[cfg(feature = "binary-protocol")] fn add_templates(tmpl_id: u16, len: u16) { "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}" } + + #[cfg(feature = "binary-protocol")] fn foreign_event_listener(event: &str, id: u32, bubbles: u8) { r#" bubbles = bubbles == 1; @@ -175,12 +202,18 @@ mod js { }); }"# } + + /// Assign the ID + #[cfg(feature = "binary-protocol")] fn assign_id_ref(array: &[u8], id: u32) { - "{this.nodes[$id$] = this.LoadChild($array$);}" + "{this.nodes[$id$] = this.loadChild($array$);}" } + + /// The coolest ID ever! + #[cfg(feature = "binary-protocol")] fn hydrate_text_ref(array: &[u8], value: &str, id: u32) { r#"{ - let node = this.LoadChild($array$); + let node = this.loadChild($array$); if (node.nodeType == node.TEXT_NODE) { node.textContent = value; } else { @@ -191,38 +224,9 @@ mod js { this.nodes[$id$] = node; }"# } + + #[cfg(feature = "binary-protocol")] fn replace_placeholder_ref(array: &[u8], n: u16) { - "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}" - } -} - -/// Extensions to the interpreter that are specific to the web platform. -#[cfg(feature = "webonly")] -#[wasm_bindgen(module = "src/js/web.js")] -extern "C" { - pub type WebInterpreter; - - #[wasm_bindgen(method)] - pub fn initialize(this: &WebInterpreter, root: Node, handler: &js_sys::Function); - - #[wasm_bindgen(method, js_name = "saveTemplate")] - pub fn save_template(this: &WebInterpreter, nodes: Vec, tmpl_id: u16); - - #[wasm_bindgen(method)] - pub fn hydrate(this: &WebInterpreter, ids: Vec); - - #[wasm_bindgen(method, js_name = "getNode")] - pub fn get_node(this: &WebInterpreter, id: u32) -> Node; -} - -#[cfg(feature = "webonly")] -type PlatformInterpreter = WebInterpreter; - -#[cfg(feature = "webonly")] -impl Interpreter { - /// Convert the interpreter to a web interpreter, enabling methods like hydrate and save_template. - pub fn as_web(&self) -> &WebInterpreter { - use wasm_bindgen::prelude::JsCast; - &self.js_channel().unchecked_ref() + "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($array$); node.replaceWith(...this.els);}" } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ab5521053..f975fd738 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,7 +8,7 @@ use dioxus_core::ElementId; use dioxus_html::PlatformEventData; -use dioxus_interpreter_js::{unified_bindings::Interpreter, WebInterpreter}; +use dioxus_interpreter_js::unified_bindings::Interpreter; use futures_channel::mpsc; use rustc_hash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; @@ -112,7 +112,7 @@ impl WebsysDom { } })); - let _interpreter: &WebInterpreter = interpreter.as_web(); + let _interpreter = interpreter.base(); _interpreter.initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index 19139e1ed..dc7d91e75 100644 --- a/packages/web/src/mutations.rs +++ b/packages/web/src/mutations.rs @@ -65,7 +65,7 @@ impl WebsysDom { #[cfg(feature = "mounted")] fn flush_queued_mounted_events(&mut self) { for id in self.queued_mounted_events.drain(..) { - let node = self.interpreter.as_web().get_node(id.0 as u32); + let node = self.interpreter.base().get_node(id.0 as u32); if let Some(element) = node.dyn_ref::() { let _ = self.event_channel.unbounded_send(UiEvent { name: "mounted".to_string(), @@ -92,7 +92,7 @@ impl WriteMutations for WebsysDom { self.templates .insert(template.name.to_owned(), self.max_template_id); self.interpreter - .as_web() + .base() .save_template(roots, self.max_template_id); self.max_template_id += 1 } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 3627ad0aa..de137873a 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -22,7 +22,7 @@ impl WebsysDom { // Recursively rehydrate the dom from the VirtualDom self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?; - self.interpreter.as_web().hydrate(ids); + self.interpreter.base().hydrate(ids); #[cfg(feature = "mounted")] for id in to_mount { @@ -168,7 +168,7 @@ impl WriteMutations for OnlyWriteTemplates<'_> { .insert(template.name.to_owned(), self.0.max_template_id); self.0 .interpreter - .as_web() + .base() .save_template(roots, self.0.max_template_id); self.0.max_template_id += 1 }