From f20b740abefd3181589125dee882b4085013be44 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 9 Oct 2023 14:28:12 -0500 Subject: [PATCH] switch liveview to sledgehammer --- packages/desktop/src/desktop_context.rs | 19 + packages/interpreter/Cargo.toml | 6 +- packages/interpreter/src/bindings.rs | 83 --- packages/interpreter/src/interpreter.js | 651 ++++++++---------- packages/interpreter/src/lib.rs | 8 +- .../interpreter/src/sledgehammer_bindings.rs | 173 ++++- packages/liveview/.gitignore | 1 + packages/liveview/Cargo.toml | 11 +- packages/liveview/build.rs | 63 ++ packages/liveview/examples/axum_stress.rs | 65 ++ .../liveview/src/adapters/axum_adapter.rs | 2 +- packages/liveview/src/lib.rs | 60 +- packages/liveview/src/main.js | 37 +- packages/liveview/src/pool.rs | 175 ++++- packages/web/Cargo.toml | 1 + packages/web/src/dom.rs | 18 +- packages/web/src/lib.rs | 1 + 17 files changed, 791 insertions(+), 583 deletions(-) delete mode 100644 packages/interpreter/src/bindings.rs create mode 100644 packages/liveview/.gitignore create mode 100644 packages/liveview/build.rs create mode 100644 packages/liveview/examples/axum_stress.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 897f8eba8..0e8b3e731 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -14,6 +14,7 @@ use dioxus_core::ScopeState; use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; +use dioxus_interpreter_js::Channel; use slab::Slab; use wry::application::accelerator::Accelerator; use wry::application::event::Event; @@ -35,6 +36,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } +struct EditQueue { + queue: Vec>, +} + +impl EditQueue { + fn push(&mut self, channel: &mut Channel) { + let iter = channel.export_memory(); + self.queue.push(iter.collect()); + channel.reset(); + } +} + pub(crate) type WebviewQueue = Rc>>; /// An imperative interface to the current window. @@ -67,6 +80,10 @@ pub struct DesktopService { pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) event_queue: Rc>>>, + + pub(crate) channel: Channel, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -100,6 +117,8 @@ impl DesktopService { pending_windows: webviews, event_handlers, shortcut_manager, + event_queue: Rc::new(RefCell::new(Vec::new())), + channel: Channel::new(), #[cfg(target_os = "ios")] views: Default::default(), } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7298cd4bd..7dea0fe79 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,13 +14,13 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { workspace = true, 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.2.1", optional = true } +sledgehammer_bindgen = { path = "/Users/evanalmloff/Desktop/Github/sledgehammer_bindgen", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] serialize = ["serde"] -web = ["wasm-bindgen", "js-sys", "web-sys"] -sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] +sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"] +web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"] minimal_bindings = [] diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs deleted file mode 100644 index 4fd9016f1..000000000 --- a/packages/interpreter/src/bindings.rs +++ /dev/null @@ -1,83 +0,0 @@ -#![allow(clippy::unused_unit, non_upper_case_globals)] - -use js_sys::Function; -use wasm_bindgen::prelude::*; -use web_sys::Element; - -#[wasm_bindgen(module = "/src/interpreter.js")] -extern "C" { - pub type InterpreterConfig; - #[wasm_bindgen(constructor)] - pub fn new(intercept_link_redirects: bool) -> InterpreterConfig; - - pub type Interpreter; - - #[wasm_bindgen(constructor)] - pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter; - - #[wasm_bindgen(method)] - pub fn SaveTemplate(this: &Interpreter, template: JsValue); - - #[wasm_bindgen(method)] - pub fn MountToRoot(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); - - #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32); - - #[wasm_bindgen(method)] - pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32); - - #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32); - - #[wasm_bindgen(method)] - pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32); - - #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool); - - #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, id: u32, text: JsValue); - - #[wasm_bindgen(method)] - pub fn NewEventListener( - this: &Interpreter, - name: &str, - id: u32, - bubbles: bool, - handler: &Function, - ); - - #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, id: u32); - - #[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 a03983ffa..aaeb3a871 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,390 +1,11 @@ import { setAttributeInner } from "./common.js"; -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); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} - class InterpreterConfig { constructor(intercept_link_redirects) { this.intercept_link_redirects = intercept_link_redirects; } } -class Interpreter { - constructor(root, config) { - this.config = config; - this.root = root; - this.listeners = new ListenerMap(root); - this.nodes = [root]; - this.stack = [root]; - this.handlers = {}; - this.templates = {}; - this.lastNodeWasText = false; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - MountToRoot() { - this.AppendChildren(this.stack.length - 1); - } - SetNode(id, node) { - this.nodes[id] = node; - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - PopRoot() { - this.stack.pop(); - } - AppendChildren(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]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - if (is_element_node(root.nodeType)) { - this.listeners.removeAllNonBubbling(root); - } - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - if (is_element_node(node)) { - this.listeners.removeAllNonBubbling(node); - } - node.remove(); - } - } - CreateTextNode(text, root) { - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, bubbles, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, element, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners.remove(element, event_name, bubbles); - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(id, field, value, ns) { - if (value === null) { - this.RemoveAttribute(id, field, ns); - } else { - const node = this.nodes[id]; - setAttributeInner(node, field, value, ns); - } - } - RemoveAttribute(root, field, ns) { - const node = this.nodes[root]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - } - - GetClientRect(id) { - const node = this.nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - - ScrollTo(id, behavior) { - const node = this.nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; - } - - /// Set the focus on the element - SetFocus(id, focus) { - const node = this.nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; - } - - handleEdits(edits) { - for (let template of edits.templates) { - this.SaveTemplate(template); - } - - for (let edit of edits.edits) { - this.handleEdit(edit); - } - - /*POST_HANDLE_EDITS*/ - } - - SaveTemplate(template) { - let roots = []; - for (let root of template.roots) { - roots.push(this.MakeTemplateNode(root)); - } - this.templates[template.name] = roots; - } - - MakeTemplateNode(node) { - switch (node.type) { - case "Text": - return document.createTextNode(node.text); - case "Dynamic": - let dyn = document.createElement("pre"); - dyn.hidden = true; - return dyn; - case "DynamicText": - return document.createTextNode("placeholder"); - case "Element": - let el; - - if (node.namespace != null) { - el = document.createElementNS(node.namespace, node.tag); - } else { - el = document.createElement(node.tag); - } - - for (let attr of node.attrs) { - if (attr.type == "Static") { - setAttributeInner(el, attr.name, attr.value, attr.namespace); - } - } - - for (let child of node.children) { - el.appendChild(this.MakeTemplateNode(child)); - } - - return el; - } - } - AssignId(path, id) { - this.nodes[id] = this.LoadChild(path); - } - LoadChild(path) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < path.length; i++) { - node = node.childNodes[path[i]]; - } - - return node; - } - HydrateText(path, value, id) { - let node = this.LoadChild(path); - - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - // replace with a textnode - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - - this.nodes[id] = node; - } - ReplacePlaceholder(path, m) { - let els = this.stack.splice(this.stack.length - m); - let node = this.LoadChild(path); - node.replaceWith(...els); - } - LoadTemplate(name, index, id) { - let node = this.templates[name][index].cloneNode(true); - this.nodes[id] = node; - this.stack.push(node); - } - handleEdit(edit) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit.m); - break; - case "AssignId": - this.AssignId(edit.path, edit.id); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.id); - break; - case "CreateTextNode": - this.CreateTextNode(edit.value, edit.id); - break; - case "HydrateText": - this.HydrateText(edit.path, edit.value, edit.id); - break; - case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index, edit.id); - break; - case "PushRoot": - this.PushRoot(edit.id); - break; - case "ReplaceWith": - this.ReplaceWith(edit.id, edit.m); - break; - case "ReplacePlaceholder": - this.ReplacePlaceholder(edit.path, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.id, edit.m); - break; - case "InsertBefore": - this.InsertBefore(edit.id, edit.m); - break; - case "Remove": - this.Remove(edit.id); - break; - case "SetText": - this.SetText(edit.id, edit.value); - break; - case "SetAttribute": - this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.id, edit.name, edit.ns); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.id, edit.name); - break; - case "NewEventListener": - let bubbles = event_bubbles(edit.name); - - // if this is a mounted listener, we send the event immediately - if (edit.name === "mounted") { - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: edit.name, - element: edit.id, - data: null, - bubbles, - }) - ); - } else { - this.NewEventListener(edit.name, edit.id, bubbles, (event) => { - handler(event, edit.name, bubbles, this.config); - }); - } - break; - } - } -} - // this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation function handler(event, name, bubbles, config) { @@ -444,7 +65,44 @@ function handler(event, name, bubbles, config) { let contents = serialize_event(event); - /*POST_EVENT_SERIALIZATION*/ + // TODO: this should be liveview only + if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + } if ( target.tagName === "FORM" && @@ -506,6 +164,239 @@ function find_real_id(target) { return realId; } +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; + } + + create(event_name, element, bubbles, handler) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + 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] = {}; + } + 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, this.global[event_name].callback); + } + } + + 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 "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + 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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } +} +function LoadChild(array) { + // iterate through each number and get that child + node = stack[stack.length - 1]; + + for (let i = 0; i < array.length; i++) { + end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; +} +const listeners = new ListenerMap(); +let nodes = []; +let stack = []; +let root; +const templates = {}; +let node, els, end, k; +function initialize(root) { + 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]); + } +} +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, + webkitdirectory: true, +}; +function truthy(val) { + return val === "true" || val === true; +} + + +function getClientRect(id) { + const node = nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; +} + +function scrollTo(id, behavior) { + const node = nodes[id]; + if (!node) { + return false; + } + node.scrollIntoView({ + behavior: behavior, + }); + return true; +} + +/// Set the focus on the element +function setFocus(id, focus) { + const node = nodes[id]; + if (!node) { + return false; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return true; +} + +function saveTemplate(template) { + let roots = []; + for (let root of template.roots) { + roots.push(this.MakeTemplateNode(root)); + } + this.templates[template.name] = roots; +} + +function makeTemplateNode(node) { + switch (node.type) { + case "Text": + return document.createTextNode(node.text); + case "Dynamic": + let dyn = document.createElement("pre"); + dyn.hidden = true; + return dyn; + case "DynamicText": + return document.createTextNode("placeholder"); + case "Element": + let el; + + if (node.namespace != null) { + el = document.createElementNS(node.namespace, node.tag); + } else { + el = document.createElement(node.tag); + } + + for (let attr of node.attrs) { + if (attr.type == "Static") { + setAttributeInner(el, attr.name, attr.value, attr.namespace); + } + } + + for (let child of node.children) { + el.appendChild(this.MakeTemplateNode(child)); + } + + return el; + } +} + function get_mouse_data(event) { const { altKey, diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 47093c29c..650689c2c 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -6,14 +6,8 @@ mod sledgehammer_bindings; #[cfg(feature = "sledgehammer")] pub use sledgehammer_bindings::*; -#[cfg(feature = "web")] -mod bindings; - -#[cfg(feature = "web")] -pub use bindings::Interpreter; - // Common bindings for minimal usage. -#[cfg(feature = "minimal_bindings")] +#[cfg(all(feature = "minimal_bindings", feature = "web"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; #[wasm_bindgen(module = "/src/common.js")] diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 7d5f44f7d..bf9818f00 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "web")] use js_sys::Function; use sledgehammer_bindgen::bindgen; +#[cfg(feature = "web")] use web_sys::Node; -#[bindgen] +pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; +#[cfg(feature = "web")] +#[bindgen(module)] mod js { const JS: &str = r#" class ListenerMap { @@ -123,7 +127,7 @@ mod js { export function get_node(id) { return nodes[id]; } - export function initilize(root, handler) { + export function initialize(root, handler) { listeners.handler = handler; nodes = [root]; stack = [root]; @@ -172,7 +176,7 @@ mod js { extern "C" { #[wasm_bindgen] - pub fn save_template(nodes: Vec, tmpl_id: u32); + pub fn save_template(nodes: Vec, tmpl_id: u16); #[wasm_bindgen] pub fn set_node(id: u32, node: Node); @@ -181,7 +185,7 @@ mod js { pub fn get_node(id: u32) -> Node; #[wasm_bindgen] - pub fn initilize(root: Node, handler: &Function); + pub fn initialize(root: Node, handler: &Function); } fn mount_to_root() { @@ -190,19 +194,19 @@ mod js { fn push_root(root: u32) { "{stack.push(nodes[$root$]);}" } - fn append_children(id: u32, many: u32) { + fn append_children(id: u32, many: u16) { "{AppendChildren($id$, $many$);}" } fn pop_root() { "{stack.pop();}" } - fn replace_with(id: u32, n: u32) { + fn replace_with(id: u32, n: u16) { "{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) { + fn insert_after(id: u32, n: u16) { "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" } - fn insert_before(id: u32, n: u32) { + fn insert_before(id: u32, n: u16) { "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" } fn remove(id: u32) { @@ -273,10 +277,159 @@ mod js { nodes[$id$] = node; }"# } - fn replace_placeholder(ptr: u32, len: u8, n: u32) { + fn replace_placeholder(ptr: u32, len: u8, n: u16) { "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" } - fn load_template(tmpl_id: u32, index: u32, id: u32) { + fn load_template(tmpl_id: u16, index: u16, id: u32) { "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" } } + +#[cfg(not(feature = "web"))] +#[bindgen] +mod js { + const JS_FILE: &str = "./src/interpreter.js"; + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u16) { + "{AppendChildren($id$, $many$);}" + } + fn append_children_to_top(many: u16) { + "{ + root = stack[stack.length-many-1]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }" + } + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u16) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u16) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u16) { + "{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_element(element: &str) { + "{stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &str, ns: &str) { + "{stack.push(document.createElementNS($ns$, $element$))}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn add_placeholder() { + "{node = document.createElement('pre'); node.hidden = true; stack.push(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}`); + + // if this is a mounted listener, we send the event immediately + if (edit.name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: edit.id, + data: null, + bubbles, + }) + ); + } else { + listeners.create(event_name, node, bubbles, (event) => { + handler(event, event_name, bubbles, config); + }); + }"# + } + 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 set_top_attribute(field: &str, value: &str, ns: &str) { + "{SetAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{ + node = nodes[$id$]; + if (!ns) { + switch (field) { + case "value": + node.value = ""; + break; + case "checked": + node.checked = false; + break; + case "selected": + node.selected = false; + break; + case "dangerous_inner_html": + node.innerHTML = ""; + break; + default: + node.removeAttribute(field); + break; + } + } else if (ns == "style") { + node.style.removeProperty(name); + } else { + node.removeAttributeNS(ns, field); + } + }"# + } + fn assign_id(array: &[u8], id: u32) { + "{nodes[$id$] = LoadChild($array$);}" + } + fn hydrate_text(array: &[u8], value: &str, id: u32) { + r#"{ + node = LoadChild($array$); + 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(array: &[u8], n: u16) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u16, index: u16, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } + fn add_templates(tmpl_id: u16, len: u16) { + "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" + } +} diff --git a/packages/liveview/.gitignore b/packages/liveview/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/liveview/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index cbd7b75f5..512edd16a 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -22,9 +22,10 @@ tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" +rustc-hash = { workspace = true } dioxus-html = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-interpreter-js = { workspace = true } +dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] } dioxus-hot-reload = { workspace = true, optional = true } # warp @@ -52,6 +53,10 @@ axum = { version = "0.6.1", features = ["ws"] } salvo = { version = "0.44.1", features = ["affix", "ws"] } tower = "0.4.13" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] } +minify-js = "0.5.6" + [features] default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] @@ -61,6 +66,10 @@ hot-reload = ["dioxus-hot-reload"] name = "axum" required-features = ["axum"] +[[example]] +name = "axum_stress" +required-features = ["axum"] + [[example]] name = "salvo" required-features = ["salvo"] diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs new file mode 100644 index 000000000..87e168590 --- /dev/null +++ b/packages/liveview/build.rs @@ -0,0 +1,63 @@ +use dioxus_interpreter_js::COMMON_JS; +use dioxus_interpreter_js::SLEDGEHAMMER_JS; +use minify_js::*; + +fn main() { + let serialize_file_uploads = r#"if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + }"#; + let mut interpreter = + SLEDGEHAMMER_JS.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads); + while let Some(import_start) = interpreter.find("import") { + let import_end = interpreter[import_start..] + .find(|c| c == ';' || c == '\n') + .map(|i| i + import_start) + .unwrap_or_else(|| interpreter.len()); + interpreter.replace_range(import_start..import_end, ""); + } + + let main_js = std::fs::read_to_string("src/main.js").unwrap(); + + let js = format!("{interpreter}\n{main_js}"); + // std::fs::write("src/minified.js", &js).unwrap(); + + let session = Session::new(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); + std::fs::write("src/minified.js", minified).unwrap(); +} diff --git a/packages/liveview/examples/axum_stress.rs b/packages/liveview/examples/axum_stress.rs new file mode 100644 index 000000000..46a4ceb10 --- /dev/null +++ b/packages/liveview/examples/axum_stress.rs @@ -0,0 +1,65 @@ +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let state = use_state(cx, || 0); + use_future(cx, (), |_| { + to_owned![state]; + async move { + loop { + state += 1; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + }); + + cx.render(rsx! { + for _ in 0..10000 { + div { + "hello axum! {state}" + } + } + }) +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = dioxus_liveview::LiveViewPool::new(); + + let app = Router::new() + .route( + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with axum +
+ {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.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 + .unwrap(); +} diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index dda37acc1..8c6f5ce31 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -20,5 +20,5 @@ fn transform_rx(message: Result) -> Result, LiveVi } async fn transform_tx(message: Vec) -> Result { - Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) + Ok(Message::Binary(message)) } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 92cce63e6..4336b432d 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -37,74 +37,18 @@ pub enum LiveViewError { SendingFailed, } -use once_cell::sync::Lazy; - -static INTERPRETER_JS: Lazy = Lazy::new(|| { - let interpreter = dioxus_interpreter_js::INTERPRETER_JS; - let serialize_file_uploads = r#"if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const files = target.files; - const file_contents = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - let file_engine = { - files: file_contents, - }; - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); - } - read_files(); - return; - } - }"#; - - let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads); - interpreter.replace("import { setAttributeInner } from \"./common.js\";", "") -}); - -static COMMON_JS: Lazy = Lazy::new(|| { - let common = dioxus_interpreter_js::COMMON_JS; - common.replace("export", "") -}); - -static MAIN_JS: &str = include_str!("./main.js"); +static MINIFIED: &str = include_str!("./minified.js"); /// 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 { - let js = &*INTERPRETER_JS; - let common = &*COMMON_JS; format!( r#" "# ) diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index e98d02a9c..ce6c736c7 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -7,10 +7,9 @@ function main() { class IPC { constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root, new InterpreterConfig(false)); - - let ws = new WebSocket(WS_ADDR); + initialize(root); + const ws = new WebSocket(WS_ADDR); + ws.binaryType = "arraybuffer"; function ping() { ws.send("__ping__"); @@ -27,17 +26,23 @@ class IPC { }; ws.onmessage = (message) => { - // Ignore pongs - if (message.data != "__pong__") { - const event = JSON.parse(message.data); - switch (event.type) { - case "edits": - let edits = event.data; - window.interpreter.handleEdits(edits); - break; - case "query": - Function("Eval", `"use strict";${event.data};`)(); - break; + if (message.data instanceof ArrayBuffer) { + // binary frame + run_from_bytes(message.data); + } else { + // text frame + // Ignore pongs + if (message.data != "__pong__") { + const event = JSON.parse(message.data); + switch (event.type) { + case "edits": + let edits = event.data; + window.interpreter.handleEdits(edits); + break; + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } } }; @@ -49,3 +54,5 @@ class IPC { this.ws.send(msg); } } + +main(); \ No newline at end of file diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 2fd06cec6..1c4b2e51c 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -4,9 +4,11 @@ use crate::{ query::{QueryEngine, QueryResult}, LiveViewError, }; -use dioxus_core::{prelude::*, Mutations}; -use dioxus_html::{EventData, HtmlEvent, MountedData}; +use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations}; +use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData}; +use dioxus_interpreter_js::Channel; use futures_util::{pin_mut, SinkExt, StreamExt}; +use rustc_hash::FxHashMap; use serde::Serialize; use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; @@ -107,7 +109,7 @@ impl LiveViewSocket for S where /// /// 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. +/// As long as your framework can provide a Sink and Stream of Bytes, 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(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> { @@ -120,20 +122,31 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li rx }; + let mut templates: FxHashMap = Default::default(); + let mut max_template_count = 0; + // Create the a proxy for query engine let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); let query_engine = QueryEngine::new(query_tx); vdom.base_scope().provide_context(query_engine.clone()); init_eval(vdom.base_scope()); - // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); - // pin the futures so we can use select! pin_mut!(ws); - // send the initial render to the client - ws.send(edits.into_bytes()).await?; + let mut edit_channel = Channel::default(); + if let Some(edits) = { + let mutations = vdom.rebuild(); + apply_edits( + mutations, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + // 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 @@ -160,7 +173,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li match evt.as_ref().map(|o| o.as_deref()) { // respond with a pong every ping to keep the websocket alive Some(Ok(b"__ping__")) => { - ws.send(b"__pong__".to_vec()).await?; + // ws.send(b"__pong__".to_vec()).await?; } Some(Ok(evt)) => { if let Ok(message) = serde_json::from_str::(&String::from_utf8_lossy(evt)) { @@ -199,7 +212,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // handle any new queries Some(query) = query_rx.recv() => { - ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?; + // ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?; } Some(msg) = hot_reload_wait => { @@ -221,15 +234,145 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send( - serde_json::to_string(&ClientUpdate::Edits(edits)) - .unwrap() - .into_bytes(), - ) - .await?; + if let Some(edits) = { + apply_edits( + edits, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + ws.send(edits).await?; + } } } +fn add_template( + template: &Template, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) { + for (idx, root) in template.roots.iter().enumerate() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), *max_template_count); + } + channel.add_templates(*max_template_count, template.roots.len() as u16); + + *max_template_count += 1 +} + +fn create_template_node(channel: &mut Channel, v: &TemplateNode) { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } +} + +fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id as u16, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + let bytes: Vec<_> = channel.export_memory().collect(); + channel.reset(); + Some(bytes) +} + #[derive(Serialize)] #[serde(tag = "type", content = "data")] enum ClientUpdate<'a> { diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 35eb9c5f1..428815576 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -15,6 +15,7 @@ dioxus-html = { workspace = true, features = ["wasm-bind"] } dioxus-interpreter-js = { workspace = true, features = [ "sledgehammer", "minimal_bindings", + "web", ] } js-sys = "0.3.56" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4fa5c9444..2f5ce30f1 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -25,8 +25,8 @@ pub struct WebsysDom { document: Document, #[allow(dead_code)] pub(crate) root: Element, - templates: FxHashMap, - max_template_id: u32, + templates: FxHashMap, + max_template_id: u16, pub(crate) interpreter: Channel, event_channel: mpsc::UnboundedSender, } @@ -90,7 +90,7 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initilize( + dioxus_interpreter_js::initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), ); @@ -175,7 +175,7 @@ impl WebsysDom { let mut to_mount = Vec::new(); for edit in &edits { match edit { - AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), + AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u16), AssignId { path, id } => { i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32) } @@ -186,15 +186,15 @@ impl WebsysDom { } 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) + i.load_template(*tmpl_id, *index as u16, id.0 as u32) } } - ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32), + ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u16), ReplacePlaceholder { path, m } => { - i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32) + i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u16) } - 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), + InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u16), + InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u16), SetAttribute { name, value, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 60f84de37..4a70182f2 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,6 +54,7 @@ // - Do DOM work in the next requestAnimationFrame callback pub use crate::cfg::Config; +#[cfg(feature = "file_engine")] pub use crate::file_engine::WebFileEngineExt; use dioxus_core::{Element, Scope, VirtualDom}; use futures_util::{