From f20b740abefd3181589125dee882b4085013be44 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 9 Oct 2023 14:28:12 -0500 Subject: [PATCH 01/21] 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::{ From 1a4741ce0449c5f219e7990333cc275208621b8e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 12 Oct 2023 10:21:29 -0500 Subject: [PATCH 02/21] fix liveview events --- packages/core/src/mutations.rs | 2 +- packages/interpreter/src/sledgehammer_bindings.rs | 11 ++++++----- packages/liveview/src/main.js | 2 ++ packages/liveview/src/pool.rs | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 27976c157..e11ef4922 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -26,7 +26,7 @@ pub struct Mutations<'a> { /// Any templates encountered while diffing the DOM. /// /// These must be loaded into a cache before applying the edits - pub templates: Vec>, + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index bf9818f00..6726b48f6 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -288,7 +288,7 @@ mod js { #[cfg(not(feature = "web"))] #[bindgen] mod js { - const JS_FILE: &str = "./src/interpreter.js"; + const JS_FILE: &str = "./packages/interpreter/src/interpreter.js"; fn mount_to_root() { "{AppendChildren(root, stack.length-1);}" @@ -329,10 +329,10 @@ mod js { fn create_text_node(text: &str, id: u32) { "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" } - fn create_element(element: &str) { + fn create_element(element: &'static str) { "{stack.push(document.createElement($element$))}" } - fn create_element_ns(element: &str, ns: &str) { + fn create_element_ns(element: &'static str, ns: &'static str) { "{stack.push(document.createElementNS($ns$, $element$))}" } fn create_placeholder(id: u32) { @@ -343,6 +343,7 @@ mod js { } fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { r#" + bubbles = bubbles == 1; node = nodes[id]; if(node.listening){ node.listening += 1; @@ -352,10 +353,10 @@ mod js { node.setAttribute('data-dioxus-id', `\${id}`); // if this is a mounted listener, we send the event immediately - if (edit.name === "mounted") { + if (event_name === "mounted") { window.ipc.postMessage( serializeIpcMessage("user_event", { - name: edit.name, + name: event_name, element: edit.id, data: null, bubbles, diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index ce6c736c7..4ca557127 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -1,3 +1,5 @@ +const config = new InterpreterConfig(false); + function main() { let root = window.document.getElementById("main"); if (root != null) { diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 1c4b2e51c..a74c183df 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -248,7 +248,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li } fn add_template( - template: &Template, + template: &Template<'static>, channel: &mut Channel, templates: &mut FxHashMap, max_template_count: &mut u16, @@ -262,7 +262,7 @@ fn add_template( *max_template_count += 1 } -fn create_template_node(channel: &mut Channel, v: &TemplateNode) { +fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { use TemplateNode::*; match v { Element { From 3063d83406ccb842ba35fe908c0989b6b7ac420e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 13 Oct 2023 09:21:57 -0500 Subject: [PATCH 03/21] start migrating desktop to sledgehammer --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/desktop_context.rs | 1 - packages/desktop/src/protocol.rs | 3 ++- packages/desktop/src/webview.rs | 10 ++++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index a884928ba..3d147f253 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } -dioxus-interpreter-js = { workspace = true } +dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"]} dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0e8b3e731..b8dff7125 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -14,7 +14,6 @@ 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; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 53652f82f..4efe085bd 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -53,10 +53,11 @@ fn module_loader(root_name: &str) -> String { pub(super) fn desktop_handler( request: &Request>, + responder: wry::http::response::Responder, custom_head: Option, custom_index: Option, root_name: &str, -) -> Result>> { +) { // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { // If a custom index is provided, just defer to that, expecting the user to know what they're doing. diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 469aa8068..673f5d96f 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -44,8 +44,14 @@ pub fn build( _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); } }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name) + .with_asynchronous_custom_protocol("dioxus".into(), move |r, responder| { + protocol::desktop_handler( + r, + responder, + custom_head.clone(), + index_file.clone(), + &root_name, + ) }) .with_file_drop_handler(move |window, evet| { file_handler From 2645b855337badce736734c8990e7bbf0ba44850 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 15 Oct 2023 09:23:01 -0500 Subject: [PATCH 04/21] WIP desktop binary protocol --- packages/desktop/Cargo.toml | 3 +- packages/desktop/src/cfg.rs | 5 +- packages/desktop/src/desktop_context.rs | 42 +++++-- packages/desktop/src/lib.rs | 156 ++++++++++++++++++++++-- packages/desktop/src/protocol.rs | 67 ++++++++-- packages/desktop/src/webview.rs | 29 +++-- 6 files changed, 259 insertions(+), 43 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 2c3dd2496..66ac4be4b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -18,7 +18,7 @@ dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } -wry = { version = "0.31.0", default-features = false, features = ["protocol", "file-drop"] } +wry = { version = "0.33.0", default-features = false, features = ["protocol", "file-drop", "tao"] } tracing = { workspace = true } futures-channel = { workspace = true } tokio = { workspace = true, features = [ @@ -33,6 +33,7 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" slab = { workspace = true } +rustc-hash = { workspace = true } futures-util = { workspace = true } urlencoding = "2.1.2" diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 07281e364..00f4847c4 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -6,7 +6,6 @@ use wry::{ application::window::{Window, WindowBuilder}, http::{Request as HttpRequest, Response as HttpResponse}, webview::FileDropEvent, - Result as WryResult, }; // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); @@ -42,7 +41,7 @@ type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, - Box>) -> WryResult>> + 'static>, + Box>) -> HttpResponse> + 'static>, ); impl Config { @@ -120,7 +119,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&HttpRequest>) -> WryResult>> + 'static, + F: Fn(HttpRequest>) -> HttpResponse> + 'static, { self.protocols.push((name, Box::new(handler))); self diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 9cd46523e..45ae6cefc 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,6 +1,8 @@ use std::cell::RefCell; use std::rc::Rc; use std::rc::Weak; +use std::sync::Arc; +use std::sync::Mutex; use crate::create_new_window; use crate::events::IpcMessage; @@ -12,6 +14,8 @@ 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 rustc_hash::FxHashMap; use slab::Slab; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; @@ -32,14 +36,33 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } -struct EditQueue { - queue: Vec>, +/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events. +/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made. +#[derive(Default, Clone)] +pub(crate) struct EditQueue { + queue: Arc>>>, + responder: Arc>>, } impl EditQueue { - fn push(&mut self, channel: &mut Channel) { + pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { + let mut queue = self.queue.lock().unwrap(); + if let Some(bytes) = queue.pop() { + responder.respond(wry::http::Response::new(bytes)); + } else { + *self.responder.lock().unwrap() = Some(responder); + } + } + + pub fn add_edits(&self, channel: &mut Channel) { let iter = channel.export_memory(); - self.queue.push(iter.collect()); + let bytes = iter.collect(); + let mut responder = self.responder.lock().unwrap(); + if let Some(responder) = responder.take() { + responder.respond(wry::http::Response::new(bytes)); + } else { + self.queue.lock().unwrap().push(bytes); + } channel.reset(); } } @@ -76,7 +99,9 @@ pub struct DesktopService { pub(crate) shortcut_manager: ShortcutRegistry, - pub(crate) event_queue: Rc>>>, + pub(crate) edit_queue: EditQueue, + pub(crate) templates: FxHashMap, + pub(crate) max_template_count: u16, pub(crate) channel: Channel, @@ -104,6 +129,7 @@ impl DesktopService { webviews: WebviewQueue, event_handlers: WindowEventHandlers, shortcut_manager: ShortcutRegistry, + edit_queue: EditQueue, ) -> Self { Self { webview: Rc::new(webview), @@ -113,8 +139,10 @@ impl DesktopService { pending_windows: webviews, event_handlers, shortcut_manager, - event_queue: Rc::new(RefCell::new(Vec::new())), - channel: Channel::new(), + edit_queue, + templates: Default::default(), + max_template_count: 0, + channel: Channel::default(), #[cfg(target_os = "ios")] views: Default::default(), } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index b802dc0ef..f8170cf49 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -27,11 +27,13 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::MountedData; +use dioxus_html::{event_bubbles, MountedData}; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use dioxus_interpreter_js::Channel; use element::DesktopElement; use eval::init_eval; use futures_util::{pin_mut, FutureExt}; +use rustc_hash::FxHashMap; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::cell::Cell; @@ -305,7 +307,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) view.dom.handle_event(&name, as_any, element, bubbles); - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query @@ -328,7 +330,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); - send_edits(view.dom.rebuild(), &view.desktop_context.webview); + send_edits(view.dom.rebuild(), &view.desktop_context); } EventData::Ipc(msg) if msg.method() == "browser_open" => { @@ -366,7 +368,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) view.dom.handle_event(event_name, data, id, event_bubbles); } - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } @@ -386,7 +388,7 @@ fn create_new_window( event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { - let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); + let (webview, web_context, edit_queue) = webview::build(&mut cfg, event_loop, proxy.clone()); let desktop_context = Rc::from(DesktopService::new( webview, proxy.clone(), @@ -394,6 +396,7 @@ fn create_new_window( queue.clone(), event_handlers.clone(), shortcut_manager, + edit_queue, )); let cx = dom.base_scope(); @@ -440,16 +443,149 @@ fn poll_vdom(view: &mut WebviewHandler) { } } - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } /// Send a list of mutations to the webview -fn send_edits(edits: Mutations, webview: &WebView) { - let serialized = serde_json::to_string(&edits).unwrap(); +fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { + if edits.edits.len() > 0 || edits.templates.len() > 0 { + apply_edits( + edits, + &mut desktop_context.channel, + &mut desktop_context.templates, + &mut desktop_context.max_template_count, + ); + desktop_context + .edit_queue + .add_edits(&mut desktop_context.channel) + } +} - // todo: use SSE and binary data to send the edits with lower overhead - _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})")); +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) +} + +fn add_template( + template: &Template<'static>, + 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: &'static TemplateNode<'static>) { + 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(), + } } /// Different hide implementations per platform diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 4efe085bd..5837eed3d 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -5,9 +5,12 @@ use std::{ }; use wry::{ http::{status::StatusCode, Request, Response}, + webview::RequestAsyncResponder, Result, }; +use crate::desktop_context::EditQueue; + fn module_loader(root_name: &str) -> String { let js = INTERPRETER_JS.replace( "/*POST_HANDLE_EDITS*/", @@ -53,10 +56,11 @@ fn module_loader(root_name: &str) -> String { pub(super) fn desktop_handler( request: &Request>, - responder: wry::http::response::Responder, + responder: RequestAsyncResponder, custom_head: Option, custom_index: Option, root_name: &str, + edit_queue: &EditQueue, ) { // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { @@ -81,15 +85,30 @@ pub(super) fn desktop_handler( } }; - return Response::builder() + match Response::builder() .header("Content-Type", "text/html") .body(Cow::from(body)) - .map_err(From::from); + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } } else if request.uri().path() == "/common.js" { - return Response::builder() + match Response::builder() .header("Content-Type", "text/javascript") .body(Cow::from(COMMON_JS.as_bytes())) - .map_err(From::from); + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } + } else if request.uri().path() == "/edits" { + edit_queue.handle_request(responder); + return; } // Else, try to serve a file from the filesystem. @@ -107,16 +126,42 @@ pub(super) fn desktop_handler( } if asset.exists() { - return Response::builder() - .header("Content-Type", get_mime_from_path(&asset)?) - .body(Cow::from(std::fs::read(asset)?)) - .map_err(From::from); + let content_type = match get_mime_from_path(&asset) { + Ok(content_type) => content_type, + Err(err) => { + tracing::error!("error getting mime type: {}", err); + return; + } + }; + let asset = match std::fs::read(asset) { + Ok(asset) => asset, + Err(err) => { + tracing::error!("error reading asset: {}", err); + return; + } + }; + match Response::builder() + .header("Content-Type", content_type) + .body(Cow::from(asset)) + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } } - Response::builder() + match Response::builder() .status(StatusCode::NOT_FOUND) .body(Cow::from(String::from("Not Found").into_bytes())) - .map_err(From::from) + { + Ok(response) => { + responder.respond(response); + return; + } + Err(err) => tracing::error!("error building response: {}", err), + } } #[allow(unreachable_code)] diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 673f5d96f..0b253b497 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,6 +1,8 @@ -use crate::desktop_context::EventData; +use crate::desktop_context::{EditQueue, EventData}; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; +use std::sync::Arc; +use std::sync::Mutex; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; @@ -11,7 +13,7 @@ pub fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> (WebView, WebContext) { +) -> (WebView, WebContext, EditQueue) { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); @@ -32,6 +34,7 @@ pub fn build( } let mut web_context = WebContext::new(cfg.data_dir.clone()); + let edit_queue = EditQueue::default(); let mut webview = WebViewBuilder::new(window) .unwrap() @@ -44,14 +47,18 @@ pub fn build( _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); } }) - .with_asynchronous_custom_protocol("dioxus".into(), move |r, responder| { - protocol::desktop_handler( - r, - responder, - custom_head.clone(), - index_file.clone(), - &root_name, - ) + .with_asynchronous_custom_protocol("dioxus".into(), { + let edit_queue = edit_queue.clone(); + move |r, responder| { + protocol::desktop_handler( + &r, + responder, + custom_head.clone(), + index_file.clone(), + &root_name, + &edit_queue, + ) + } }) .with_file_drop_handler(move |window, evet| { file_handler @@ -100,5 +107,5 @@ pub fn build( webview = webview.with_devtools(true); } - (webview.build().unwrap(), web_context) + (webview.build().unwrap(), web_context, edit_queue) } From db56962eea6eb803b2df7b8732b0996bd8b6ea7b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Oct 2023 14:31:58 -0500 Subject: [PATCH 05/21] basic example working with binary protocol on desktop --- packages/desktop/Cargo.toml | 4 + packages/desktop/build.rs | 50 + packages/desktop/src/desktop_context.rs | 47 +- packages/desktop/src/lib.rs | 33 +- packages/desktop/src/minified.js | 1051 +++++++++++++++++ packages/desktop/src/protocol.rs | 70 +- packages/desktop/src/webview.rs | 2 - .../fullstack/src/adapters/warp_adapter.rs | 4 +- packages/liveview/build.rs | 6 +- 9 files changed, 1182 insertions(+), 85 deletions(-) create mode 100644 packages/desktop/build.rs create mode 100644 packages/desktop/src/minified.js diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 66ac4be4b..57326db98 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -73,6 +73,10 @@ dioxus = { workspace = true } exitcode = "1.1.2" scraper = "0.16.0" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] } +minify-js = "0.5.6" + # These tests need to be run on the main thread, so they cannot use rust's test harness. [[test]] name = "check_events" diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs new file mode 100644 index 000000000..77930cb8c --- /dev/null +++ b/packages/desktop/build.rs @@ -0,0 +1,50 @@ +use dioxus_interpreter_js::SLEDGEHAMMER_JS; + +use std::io::Write; + +fn main() { + let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click + let inputs = document.querySelectorAll("input"); + for (let input of inputs) { + if (!input.getAttribute("data-dioxus-file-listener")) { + // prevent file inputs from opening the file dialog on click + const type = input.getAttribute("type"); + if (type === "file") { + input.setAttribute("data-dioxus-file-listener", true); + input.addEventListener("click", (event) => { + let target = event.target; + let target_id = find_real_id(target); + if (target_id !== null) { + const send = (event_name) => { + const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + window.ipc.postMessage(message); + }; + send("change&input"); + } + event.preventDefault(); + }); + } + } + }"#; + let mut interpreter = SLEDGEHAMMER_JS.replace("/*POST_HANDLE_EDITS*/", prevent_file_upload); + 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 js = format!("{interpreter}\nconst config = new InterpreterConfig(false);"); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(js.as_bytes()).unwrap(); + + // TODO: Enable minification on desktop + // use minify_js::*; + // 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(); + // let mut file = std::fs::File::create("src/minified.js").unwrap(); + // file.write_all(minified.as_bytes()).unwrap(); +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 45ae6cefc..cc55134d9 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,9 +1,3 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::rc::Weak; -use std::sync::Arc; -use std::sync::Mutex; - use crate::create_new_window; use crate::events::IpcMessage; use crate::query::QueryEngine; @@ -17,6 +11,14 @@ use dioxus_hot_reload::HotReloadMsg; use dioxus_interpreter_js::Channel; use rustc_hash::FxHashMap; use slab::Slab; +use std::cell::RefCell; +use std::fmt::Debug; +use std::fmt::Formatter; +use std::rc::Rc; +use std::rc::Weak; +use std::sync::atomic::AtomicU16; +use std::sync::Arc; +use std::sync::Mutex; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopWindowTarget; @@ -44,8 +46,20 @@ pub(crate) struct EditQueue { responder: Arc>>, } +impl Debug for EditQueue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EditQueue") + .field("queue", &self.queue) + .field("responder", { + &self.responder.lock().unwrap().as_ref().map(|_| ()) + }) + .finish() + } +} + impl EditQueue { pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { + println!("handling request {self:?}"); let mut queue = self.queue.lock().unwrap(); if let Some(bytes) = queue.pop() { responder.respond(wry::http::Response::new(bytes)); @@ -54,16 +68,15 @@ impl EditQueue { } } - pub fn add_edits(&self, channel: &mut Channel) { - let iter = channel.export_memory(); - let bytes = iter.collect(); + pub fn add_edits(&self, edits: Vec) { + println!("adding edits {self:?}"); let mut responder = self.responder.lock().unwrap(); if let Some(responder) = responder.take() { - responder.respond(wry::http::Response::new(bytes)); + println!("responding with {edits:?}"); + responder.respond(wry::http::Response::new(edits)); } else { - self.queue.lock().unwrap().push(bytes); + self.queue.lock().unwrap().push(edits); } - channel.reset(); } } @@ -100,10 +113,10 @@ pub struct DesktopService { pub(crate) shortcut_manager: ShortcutRegistry, pub(crate) edit_queue: EditQueue, - pub(crate) templates: FxHashMap, - pub(crate) max_template_count: u16, + pub(crate) templates: RefCell>, + pub(crate) max_template_count: AtomicU16, - pub(crate) channel: Channel, + pub(crate) channel: RefCell, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -141,8 +154,8 @@ impl DesktopService { shortcut_manager, edit_queue, templates: Default::default(), - max_template_count: 0, - channel: Channel::default(), + max_template_count: Default::default(), + channel: Default::default(), #[cfg(target_os = "ios")] views: Default::default(), } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f8170cf49..406d6ffb8 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -38,6 +38,7 @@ use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::cell::Cell; use std::rc::Rc; +use std::sync::atomic::AtomicU16; use std::task::Waker; use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; @@ -449,16 +450,15 @@ fn poll_vdom(view: &mut WebviewHandler) { /// Send a list of mutations to the webview fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { - if edits.edits.len() > 0 || edits.templates.len() > 0 { - apply_edits( - edits, - &mut desktop_context.channel, - &mut desktop_context.templates, - &mut desktop_context.max_template_count, - ); - desktop_context - .edit_queue - .add_edits(&mut desktop_context.channel) + let mut channel = desktop_context.channel.borrow_mut(); + let mut templates = desktop_context.templates.borrow_mut(); + if let Some(bytes) = apply_edits( + edits, + &mut channel, + &mut templates, + &desktop_context.max_template_count, + ) { + desktop_context.edit_queue.add_edits(bytes) } } @@ -466,7 +466,7 @@ fn apply_edits( mutations: Mutations, channel: &mut Channel, templates: &mut FxHashMap, - max_template_count: &mut u16, + max_template_count: &AtomicU16, ) -> Option> { use dioxus_core::Mutation::*; if mutations.templates.is_empty() && mutations.edits.is_empty() { @@ -538,15 +538,16 @@ fn add_template( template: &Template<'static>, channel: &mut Channel, templates: &mut FxHashMap, - max_template_count: &mut u16, + max_template_count: &AtomicU16, ) { - for (idx, root) in template.roots.iter().enumerate() { + let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); + for root in template.roots.iter() { create_template_node(channel, root); - templates.insert(template.name.to_owned(), *max_template_count); + templates.insert(template.name.to_owned(), current_max_template_count); } - channel.add_templates(*max_template_count, template.roots.len() as u16); + channel.add_templates(current_max_template_count, template.roots.len() as u16); - *max_template_count += 1 + max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); } fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { diff --git a/packages/desktop/src/minified.js b/packages/desktop/src/minified.js new file mode 100644 index 000000000..b1ef5e4d1 --- /dev/null +++ b/packages/desktop/src/minified.js @@ -0,0 +1,1051 @@ +let m,p,ls,d,t,op,i,e,z,metaflags; + ; + +class InterpreterConfig { + constructor(intercept_link_redirects) { + this.intercept_link_redirects = intercept_link_redirects; + } +} + +// 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) { + let target = event.target; + if (target != null) { + let preventDefaultRequests = null; + // Some events can be triggered on text nodes, which don't have attributes + if (target instanceof Element) { + preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); + } + + if (event.type === "click") { + // todo call prevent default if it's the right type of event + if (config.intercept_link_redirects) { + let a_element = target.closest("a"); + if (a_element != null) { + event.preventDefault(); + + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); + + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.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(); + } + } + + const realId = find_real_id(target); + + if ( + preventDefaultRequests && + preventDefaultRequests.includes(`on${event.type}`) + ) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + let contents = serialize_event(event); + + // 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" && + (event.type === "submit" || event.type === "input") + ) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + let value = formData.getAll(name); + contents.values[name] = value; + } + } + + if ( + target.tagName === "SELECT" && + event.type === "input" + ) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + + if (realId === null) { + return; + } + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }) + ); + } +} + +function find_real_id(target) { + let realId = null; + if (target instanceof Element) { + realId = target.getAttribute(`data-dioxus-id`); + } + // 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; + if (target instanceof Element) { + realId = target.getAttribute(`data-dioxus-id`); + } + } + 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, + 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, + }; +} + +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 "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": { + return { mouse: get_mouse_data(event) }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "dblclick": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + return get_mouse_data(event); + } + 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 }); +} + +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": + case "loadedmetadata": + case "loadstart": + case "load": + 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; + case "mounted": + return false; + } + + return true; +} +let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf,u32bufp;const attr = []; + let attr_cache_hit, attr_cache_idx; + function get_attr() { + attr_cache_idx = u8buf[u8bufp++]; + if(attr_cache_idx & 128){ + attr_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + attr[attr_cache_idx&4294967167]=attr_cache_hit; + return attr_cache_hit; + } + else{ + return attr[attr_cache_idx&4294967167]; + } + }const evt = []; + let evt_cache_hit, evt_cache_idx; + function get_evt() { + evt_cache_idx = u8buf[u8bufp++]; + if(evt_cache_idx & 128){ + evt_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + evt[evt_cache_idx&4294967167]=evt_cache_hit; + return evt_cache_hit; + } + else{ + return evt[evt_cache_idx&4294967167]; + } + }const ns_cache = []; + let ns_cache_cache_hit, ns_cache_cache_idx; + function get_ns_cache() { + ns_cache_cache_idx = u8buf[u8bufp++]; + if(ns_cache_cache_idx & 128){ + ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit; + return ns_cache_cache_hit; + } + else{ + return ns_cache[ns_cache_cache_idx&4294967167]; + } + }const el = []; + let el_cache_hit, el_cache_idx; + function get_el() { + el_cache_idx = u8buf[u8bufp++]; + if(el_cache_idx & 128){ + el_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + el[el_cache_idx&4294967167]=el_cache_hit; + return el_cache_hit; + } + else{ + return el[el_cache_idx&4294967167]; + } + }const namespace = []; + let namespace_cache_hit, namespace_cache_idx; + function get_namespace() { + namespace_cache_idx = u8buf[u8bufp++]; + if(namespace_cache_idx & 128){ + namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + namespace[namespace_cache_idx&4294967167]=namespace_cache_hit; + return namespace_cache_hit; + } + else{ + return namespace[namespace_cache_idx&4294967167]; + } + }let u16buf,u16bufp; + let value,event_name,array,ns,many,bubbles,id,field; + function create(r){ + d=r; + } + function update_memory(b){ + m=new DataView(b.buffer) + } + function run(){ + metaflags=m.getUint32(d,true); + if((metaflags>>>6)&1){ + ls=m.getUint32(d+6*4,true); + } + p=ls; + if ((metaflags>>>5)&1){ + t = m.getUint32(d+5*4,true); + u8buf=new Uint8Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%1)/1); + } + u8bufp=0;if (metaflags&1){ + lsp = m.getUint32(d+1*4,true); + } + if ((metaflags>>>2)&1) { + sl = m.getUint32(d+2*4,true); + if ((metaflags>>>1)&1) { + sp = lsp; + s = ""; + e = sp + ((sl / 4) | 0) * 4; + while (sp < e) { + t = m.getUint32(sp, true); + s += String.fromCharCode( + t & 255, + (t & 65280) >> 8, + (t & 16711680) >> 16, + t >> 24 + ); + sp += 4; + } + while (sp < lsp + sl) { + s += String.fromCharCode(m.getUint8(sp++)); + } + } else { + s = c.decode(new DataView(m.buffer, lsp, sl)); + } + } + sp=0;if ((metaflags>>>3)&1){ + t = m.getUint32(d+3*4,true); + u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4); + } + u32bufp=0;if ((metaflags>>>4)&1){ + t = m.getUint32(d+4*4,true); + u16buf=new Uint16Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%2)/2); + } + u16bufp=0; + for(;;){ + op=m.getUint32(p,true); + p+=4; + z=0; + while(z++<4){ + switch(op&255){ + case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[u32buf[u32bufp++]]);}break;case 2:{AppendChildren(u32buf[u32bufp++], u16buf[u16bufp++]);}break;case 3:many=u16buf[u16bufp++];{ + root = stack[stack.length-many-1]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }break;case 4:{stack.pop();}break;case 5:{root = nodes[u32buf[u32bufp++]]; els = stack.splice(stack.length-u16buf[u16bufp++]); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}break;case 6:{nodes[u32buf[u32bufp++]].after(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 7:{nodes[u32buf[u32bufp++]].before(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 8:{node = nodes[u32buf[u32bufp++]]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}break;case 9:{stack.push(document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])));}break;case 10:{node = document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 11:{stack.push(document.createElement(get_el()))}break;case 12:{stack.push(document.createElementNS(get_namespace(), get_el()))}break;case 13:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[u32buf[u32bufp++]] = node;}break;case 14:{node = document.createElement('pre'); node.hidden = true; stack.push(node);}break;case 15:event_name=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++]; + bubbles = bubbles == 1; + 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 (event_name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: event_name, + element: edit.id, + data: null, + bubbles, + }) + ); + } else { + listeners.create(event_name, node, bubbles, (event) => { + handler(event, event_name, bubbles, config); + }); + }break;case 16:{node = nodes[u32buf[u32bufp++]]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, get_evt(), u8buf[u8bufp++]);}break;case 17:{nodes[u32buf[u32bufp++]].textContent = s.substring(sp,sp+=u32buf[u32bufp++]);}break;case 18:{node = nodes[u32buf[u32bufp++]]; SetAttributeInner(node, get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 19:{SetAttributeInner(stack[stack.length-1], get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 20:id=u32buf[u32bufp++];field=get_attr();ns=get_ns_cache();{ + 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); + } + }break;case 21:{nodes[u32buf[u32bufp++]] = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})());}break;case 22:array=(()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})();value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{ + 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; + }break;case 23:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})()); node.replaceWith(...els);}break;case 24:{node = templates[u16buf[u16bufp++]][u16buf[u16bufp++]].cloneNode(true); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 25:{templates[u16buf[u16bufp++]] = stack.splice(stack.length-u16buf[u16bufp++]);}break;case 26:return true; + } + op>>>=8; + } + } + } + function run_from_bytes(bytes){ + d = 0; + update_memory(new Uint8Array(bytes)) + run() + } +const config = new InterpreterConfig(false); \ No newline at end of file diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 5837eed3d..f8ba940f4 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,3 @@ -use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -11,43 +10,34 @@ use wry::{ use crate::desktop_context::EditQueue; +static MINIFIED: &str = include_str!("./minified.js"); + fn module_loader(root_name: &str) -> String { - let js = INTERPRETER_JS.replace( - "/*POST_HANDLE_EDITS*/", - r#"// Prevent file inputs from opening the file dialog on click - let inputs = document.querySelectorAll("input"); - for (let input of inputs) { - if (!input.getAttribute("data-dioxus-file-listener")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); - window.ipc.postMessage(message); - }; - send("change&input"); - } - event.preventDefault(); - }); - } - } - }"#, - ); format!( r#" "# @@ -87,6 +77,7 @@ pub(super) fn desktop_handler( match Response::builder() .header("Content-Type", "text/html") + .header("Access-Control-Allow-Origin", "*") .body(Cow::from(body)) { Ok(response) => { @@ -95,18 +86,7 @@ pub(super) fn desktop_handler( } Err(err) => tracing::error!("error building response: {}", err), } - } else if request.uri().path() == "/common.js" { - match Response::builder() - .header("Content-Type", "text/javascript") - .body(Cow::from(COMMON_JS.as_bytes())) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), - } - } else if request.uri().path() == "/edits" { + } else if request.uri().path().trim_matches('/') == "edits" { edit_queue.handle_request(responder); return; } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 0b253b497..44b5a3bf0 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,8 +1,6 @@ use crate::desktop_context::{EditQueue, EventData}; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; -use std::sync::Arc; -use std::sync::Mutex; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 5afcb5077..316d70e56 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -143,7 +143,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R let req = warp::hyper::Request::from_parts(parts, bytes.into()); service.run(req).await.map_err(|err| { tracing::error!("Server function error: {}", err); - + struct WarpServerFnError(String); impl std::fmt::Debug for WarpServerFnError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -152,7 +152,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R } impl warp::reject::Reject for WarpServerFnError {} - + warp::reject::custom(WarpServerFnError(err.to_string())) }) } diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs index 87e168590..fc38e29c4 100644 --- a/packages/liveview/build.rs +++ b/packages/liveview/build.rs @@ -1,6 +1,6 @@ -use dioxus_interpreter_js::COMMON_JS; use dioxus_interpreter_js::SLEDGEHAMMER_JS; use minify_js::*; +use std::io::Write; fn main() { let serialize_file_uploads = r#"if ( @@ -53,11 +53,11 @@ fn main() { 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(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); } From c8078082318604621746bd43294127b49691861d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Oct 2023 14:32:31 -0500 Subject: [PATCH 06/21] remove logging --- packages/desktop/src/desktop_context.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index cc55134d9..1b253eafc 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -59,7 +59,6 @@ impl Debug for EditQueue { impl EditQueue { pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { - println!("handling request {self:?}"); let mut queue = self.queue.lock().unwrap(); if let Some(bytes) = queue.pop() { responder.respond(wry::http::Response::new(bytes)); @@ -69,10 +68,8 @@ impl EditQueue { } pub fn add_edits(&self, edits: Vec) { - println!("adding edits {self:?}"); let mut responder = self.responder.lock().unwrap(); if let Some(responder) = responder.take() { - println!("responding with {edits:?}"); responder.respond(wry::http::Response::new(edits)); } else { self.queue.lock().unwrap().push(edits); From a4fbeeb9326e28b1e47556f611780f8f8b53c422 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Oct 2023 14:45:37 -0500 Subject: [PATCH 07/21] add stress test for desktop --- packages/desktop/examples/stress.rs | 32 ++++++++++++++++ packages/desktop/src/minified.js | 58 ++++++++++++++--------------- 2 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 packages/desktop/examples/stress.rs diff --git a/packages/desktop/examples/stress.rs b/packages/desktop/examples/stress.rs new file mode 100644 index 000000000..5f317bc99 --- /dev/null +++ b/packages/desktop/examples/stress.rs @@ -0,0 +1,32 @@ +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! { + button { + onclick: move |_| { + state.set(0); + }, + "reset" + } + for _ in 0..10000 { + div { + "hello desktop! {state}" + } + } + }) +} + +fn main() { + dioxus_desktop::launch(app); +} diff --git a/packages/desktop/src/minified.js b/packages/desktop/src/minified.js index b1ef5e4d1..4083b6411 100644 --- a/packages/desktop/src/minified.js +++ b/packages/desktop/src/minified.js @@ -853,7 +853,19 @@ function event_bubbles(event) { return true; } -let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf,u32bufp;const attr = []; +let u32buf,u32bufp;let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();const ns_cache = []; + let ns_cache_cache_hit, ns_cache_cache_idx; + function get_ns_cache() { + ns_cache_cache_idx = u8buf[u8bufp++]; + if(ns_cache_cache_idx & 128){ + ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit; + return ns_cache_cache_hit; + } + else{ + return ns_cache[ns_cache_cache_idx&4294967167]; + } + }const attr = []; let attr_cache_hit, attr_cache_idx; function get_attr() { attr_cache_idx = u8buf[u8bufp++]; @@ -877,17 +889,17 @@ let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf, else{ return evt[evt_cache_idx&4294967167]; } - }const ns_cache = []; - let ns_cache_cache_hit, ns_cache_cache_idx; - function get_ns_cache() { - ns_cache_cache_idx = u8buf[u8bufp++]; - if(ns_cache_cache_idx & 128){ - ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit; - return ns_cache_cache_hit; + }const namespace = []; + let namespace_cache_hit, namespace_cache_idx; + function get_namespace() { + namespace_cache_idx = u8buf[u8bufp++]; + if(namespace_cache_idx & 128){ + namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); + namespace[namespace_cache_idx&4294967167]=namespace_cache_hit; + return namespace_cache_hit; } else{ - return ns_cache[ns_cache_cache_idx&4294967167]; + return namespace[namespace_cache_idx&4294967167]; } }const el = []; let el_cache_hit, el_cache_idx; @@ -901,20 +913,8 @@ let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf, else{ return el[el_cache_idx&4294967167]; } - }const namespace = []; - let namespace_cache_hit, namespace_cache_idx; - function get_namespace() { - namespace_cache_idx = u8buf[u8bufp++]; - if(namespace_cache_idx & 128){ - namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - namespace[namespace_cache_idx&4294967167]=namespace_cache_hit; - return namespace_cache_hit; - } - else{ - return namespace[namespace_cache_idx&4294967167]; - } }let u16buf,u16bufp; - let value,event_name,array,ns,many,bubbles,id,field; + let bubbles,field,value,array,event_name,many,id,ns; function create(r){ d=r; } @@ -927,7 +927,11 @@ let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf, ls=m.getUint32(d+6*4,true); } p=ls; - if ((metaflags>>>5)&1){ + if ((metaflags>>>3)&1){ + t = m.getUint32(d+3*4,true); + u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4); + } + u32bufp=0;if ((metaflags>>>5)&1){ t = m.getUint32(d+5*4,true); u8buf=new Uint8Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%1)/1); } @@ -957,11 +961,7 @@ let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf, s = c.decode(new DataView(m.buffer, lsp, sl)); } } - sp=0;if ((metaflags>>>3)&1){ - t = m.getUint32(d+3*4,true); - u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4); - } - u32bufp=0;if ((metaflags>>>4)&1){ + sp=0;if ((metaflags>>>4)&1){ t = m.getUint32(d+4*4,true); u16buf=new Uint16Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%2)/2); } From 06ca8c53d8708182a1907ad9b1925273419fd8d4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 17 Oct 2023 14:47:11 -0500 Subject: [PATCH 08/21] gitignore minified.js --- packages/desktop/.gitignore | 1 + packages/desktop/src/minified.js | 1051 ------------------------------ 2 files changed, 1 insertion(+), 1051 deletions(-) create mode 100644 packages/desktop/.gitignore delete mode 100644 packages/desktop/src/minified.js diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/desktop/src/minified.js b/packages/desktop/src/minified.js deleted file mode 100644 index 4083b6411..000000000 --- a/packages/desktop/src/minified.js +++ /dev/null @@ -1,1051 +0,0 @@ -let m,p,ls,d,t,op,i,e,z,metaflags; - ; - -class InterpreterConfig { - constructor(intercept_link_redirects) { - this.intercept_link_redirects = intercept_link_redirects; - } -} - -// 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) { - let target = event.target; - if (target != null) { - let preventDefaultRequests = null; - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (config.intercept_link_redirects) { - let a_element = target.closest("a"); - if (a_element != null) { - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.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(); - } - } - - const realId = find_real_id(target); - - if ( - preventDefaultRequests && - preventDefaultRequests.includes(`on${event.type}`) - ) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - let contents = serialize_event(event); - - // 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" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - let value = formData.getAll(name); - contents.values[name] = value; - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - - if (realId === null) { - return; - } - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }) - ); - } -} - -function find_real_id(target) { - let realId = null; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - // 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; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } - } - 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, - 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, - }; -} - -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 "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": { - return { mouse: get_mouse_data(event) }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - return get_mouse_data(event); - } - 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 }); -} - -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": - case "loadedmetadata": - case "loadstart": - case "load": - 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; - case "mounted": - return false; - } - - return true; -} -let u32buf,u32bufp;let u8buf,u8bufp;let s = "";let lsp,sp,sl; let c = new TextDecoder();const ns_cache = []; - let ns_cache_cache_hit, ns_cache_cache_idx; - function get_ns_cache() { - ns_cache_cache_idx = u8buf[u8bufp++]; - if(ns_cache_cache_idx & 128){ - ns_cache_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - ns_cache[ns_cache_cache_idx&4294967167]=ns_cache_cache_hit; - return ns_cache_cache_hit; - } - else{ - return ns_cache[ns_cache_cache_idx&4294967167]; - } - }const attr = []; - let attr_cache_hit, attr_cache_idx; - function get_attr() { - attr_cache_idx = u8buf[u8bufp++]; - if(attr_cache_idx & 128){ - attr_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - attr[attr_cache_idx&4294967167]=attr_cache_hit; - return attr_cache_hit; - } - else{ - return attr[attr_cache_idx&4294967167]; - } - }const evt = []; - let evt_cache_hit, evt_cache_idx; - function get_evt() { - evt_cache_idx = u8buf[u8bufp++]; - if(evt_cache_idx & 128){ - evt_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - evt[evt_cache_idx&4294967167]=evt_cache_hit; - return evt_cache_hit; - } - else{ - return evt[evt_cache_idx&4294967167]; - } - }const namespace = []; - let namespace_cache_hit, namespace_cache_idx; - function get_namespace() { - namespace_cache_idx = u8buf[u8bufp++]; - if(namespace_cache_idx & 128){ - namespace_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - namespace[namespace_cache_idx&4294967167]=namespace_cache_hit; - return namespace_cache_hit; - } - else{ - return namespace[namespace_cache_idx&4294967167]; - } - }const el = []; - let el_cache_hit, el_cache_idx; - function get_el() { - el_cache_idx = u8buf[u8bufp++]; - if(el_cache_idx & 128){ - el_cache_hit=s.substring(sp,sp+=u8buf[u8bufp++]); - el[el_cache_idx&4294967167]=el_cache_hit; - return el_cache_hit; - } - else{ - return el[el_cache_idx&4294967167]; - } - }let u16buf,u16bufp; - let bubbles,field,value,array,event_name,many,id,ns; - function create(r){ - d=r; - } - function update_memory(b){ - m=new DataView(b.buffer) - } - function run(){ - metaflags=m.getUint32(d,true); - if((metaflags>>>6)&1){ - ls=m.getUint32(d+6*4,true); - } - p=ls; - if ((metaflags>>>3)&1){ - t = m.getUint32(d+3*4,true); - u32buf=new Uint32Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%4)/4); - } - u32bufp=0;if ((metaflags>>>5)&1){ - t = m.getUint32(d+5*4,true); - u8buf=new Uint8Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%1)/1); - } - u8bufp=0;if (metaflags&1){ - lsp = m.getUint32(d+1*4,true); - } - if ((metaflags>>>2)&1) { - sl = m.getUint32(d+2*4,true); - if ((metaflags>>>1)&1) { - sp = lsp; - s = ""; - e = sp + ((sl / 4) | 0) * 4; - while (sp < e) { - t = m.getUint32(sp, true); - s += String.fromCharCode( - t & 255, - (t & 65280) >> 8, - (t & 16711680) >> 16, - t >> 24 - ); - sp += 4; - } - while (sp < lsp + sl) { - s += String.fromCharCode(m.getUint8(sp++)); - } - } else { - s = c.decode(new DataView(m.buffer, lsp, sl)); - } - } - sp=0;if ((metaflags>>>4)&1){ - t = m.getUint32(d+4*4,true); - u16buf=new Uint16Array(m.buffer,t,((m.buffer.byteLength-t)-(m.buffer.byteLength-t)%2)/2); - } - u16bufp=0; - for(;;){ - op=m.getUint32(p,true); - p+=4; - z=0; - while(z++<4){ - switch(op&255){ - case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[u32buf[u32bufp++]]);}break;case 2:{AppendChildren(u32buf[u32bufp++], u16buf[u16bufp++]);}break;case 3:many=u16buf[u16bufp++];{ - root = stack[stack.length-many-1]; - els = stack.splice(stack.length-many); - for (k = 0; k < many; k++) { - root.appendChild(els[k]); - } - }break;case 4:{stack.pop();}break;case 5:{root = nodes[u32buf[u32bufp++]]; els = stack.splice(stack.length-u16buf[u16bufp++]); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}break;case 6:{nodes[u32buf[u32bufp++]].after(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 7:{nodes[u32buf[u32bufp++]].before(...stack.splice(stack.length-u16buf[u16bufp++]));}break;case 8:{node = nodes[u32buf[u32bufp++]]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}break;case 9:{stack.push(document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])));}break;case 10:{node = document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 11:{stack.push(document.createElement(get_el()))}break;case 12:{stack.push(document.createElementNS(get_namespace(), get_el()))}break;case 13:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[u32buf[u32bufp++]] = node;}break;case 14:{node = document.createElement('pre'); node.hidden = true; stack.push(node);}break;case 15:event_name=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++]; - bubbles = bubbles == 1; - 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 (event_name === "mounted") { - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: event_name, - element: edit.id, - data: null, - bubbles, - }) - ); - } else { - listeners.create(event_name, node, bubbles, (event) => { - handler(event, event_name, bubbles, config); - }); - }break;case 16:{node = nodes[u32buf[u32bufp++]]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, get_evt(), u8buf[u8bufp++]);}break;case 17:{nodes[u32buf[u32bufp++]].textContent = s.substring(sp,sp+=u32buf[u32bufp++]);}break;case 18:{node = nodes[u32buf[u32bufp++]]; SetAttributeInner(node, get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 19:{SetAttributeInner(stack[stack.length-1], get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 20:id=u32buf[u32bufp++];field=get_attr();ns=get_ns_cache();{ - 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); - } - }break;case 21:{nodes[u32buf[u32bufp++]] = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})());}break;case 22:array=(()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})();value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{ - 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; - }break;case 23:{els = stack.splice(stack.length - u16buf[u16bufp++]); node = LoadChild((()=>{e=u8bufp+u32buf[u32bufp++];const final_array = u8buf.slice(u8bufp,e);u8bufp=e;return final_array;})()); node.replaceWith(...els);}break;case 24:{node = templates[u16buf[u16bufp++]][u16buf[u16bufp++]].cloneNode(true); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 25:{templates[u16buf[u16bufp++]] = stack.splice(stack.length-u16buf[u16bufp++]);}break;case 26:return true; - } - op>>>=8; - } - } - } - function run_from_bytes(bytes){ - d = 0; - update_memory(new Uint8Array(bytes)) - run() - } -const config = new InterpreterConfig(false); \ No newline at end of file From c9612a085ed1a96a5d8acd573337643395efa5ae Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 19 Oct 2023 16:40:08 -0500 Subject: [PATCH 09/21] fix liveview mounted event --- packages/desktop/Cargo.toml | 4 +- packages/desktop/build.rs | 2 +- packages/desktop/src/desktop_context.rs | 2 +- packages/interpreter/Cargo.toml | 3 +- .../interpreter/src/sledgehammer_bindings.rs | 290 +++++++++--------- packages/liveview/Cargo.toml | 4 +- packages/liveview/build.rs | 10 +- packages/liveview/src/element.rs | 9 +- packages/liveview/src/main.js | 25 +- packages/liveview/src/pool.rs | 22 +- 10 files changed, 195 insertions(+), 176 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 57326db98..fcc47e4d8 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } -dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"]} +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" @@ -74,7 +74,7 @@ exitcode = "1.1.2" scraper = "0.16.0" [build-dependencies] -dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } minify-js = "0.5.6" # These tests need to be run on the main thread, so they cannot use rust's test harness. diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index 77930cb8c..bd7445637 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -1,4 +1,4 @@ -use dioxus_interpreter_js::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; use std::io::Write; diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 1b253eafc..de5eefc89 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -8,7 +8,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 dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; use std::cell::RefCell; diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7dea0fe79..8d155db6d 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,7 +14,7 @@ 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 = { path = "/Users/evanalmloff/Desktop/Github/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 } @@ -23,4 +23,5 @@ default = [] serialize = ["serde"] sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"] web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"] +binary-protocol = ["sledgehammer"] minimal_bindings = [] diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 6726b48f6..ac1fbf5f0 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -1,10 +1,13 @@ #[cfg(feature = "web")] use js_sys::Function; +#[cfg(feature = "web")] use sledgehammer_bindgen::bindgen; #[cfg(feature = "web")] use web_sys::Node; +#[cfg(feature = "web")] pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + #[cfg(feature = "web")] #[bindgen(module)] mod js { @@ -285,152 +288,157 @@ mod js { } } -#[cfg(not(feature = "web"))] -#[bindgen] -mod js { - const JS_FILE: &str = "./packages/interpreter/src/interpreter.js"; +#[cfg(feature = "binary-protocol")] +pub mod binary_protocol { + use sledgehammer_bindgen::bindgen; + pub const SLEDGEHAMMER_JS: &str = GENERATED_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: &'static str) { - "{stack.push(document.createElement($element$))}" - } - fn create_element_ns(element: &'static str, ns: &'static 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#" - bubbles = bubbles == 1; - node = nodes[id]; - if(node.listening){ - node.listening += 1; - } else { - node.listening = 1; + #[bindgen] + mod protocol_js { + const JS_FILE: &str = "./packages/interpreter/src/interpreter.js"; + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" } - node.setAttribute('data-dioxus-id', `\${id}`); - - // if this is a mounted listener, we send the event immediately - if (event_name === "mounted") { - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: event_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; + 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]); } - } else if (ns == "style") { - node.style.removeProperty(name); + }" + } + 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: &'static str) { + "{stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &'static str, ns: &'static 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#" + bubbles = bubbles == 1; + node = nodes[id]; + if(node.listening){ + node.listening += 1; } else { - node.removeAttributeNS(ns, field); + node.listening = 1; } - }"# - } - 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; + node.setAttribute('data-dioxus-id', `\${id}`); + + // if this is a mounted listener, we send the event immediately + if (event_name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: event_name, + element: id, + data: null, + bubbles, + }) + ); } 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$);}" + 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/Cargo.toml b/packages/liveview/Cargo.toml index 512edd16a..008ab48cb 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -25,7 +25,7 @@ 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, features = ["sledgehammer"] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } # warp @@ -54,7 +54,7 @@ salvo = { version = "0.44.1", features = ["affix", "ws"] } tower = "0.4.13" [build-dependencies] -dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } minify-js = "0.5.6" [features] diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs index fc38e29c4..b62a09da0 100644 --- a/packages/liveview/build.rs +++ b/packages/liveview/build.rs @@ -1,4 +1,4 @@ -use dioxus_interpreter_js::SLEDGEHAMMER_JS; +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; use minify_js::*; use std::io::Write; @@ -55,9 +55,9 @@ fn main() { let js = format!("{interpreter}\n{main_js}"); 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(); + // let mut out = Vec::new(); + // minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + // let minified = String::from_utf8(out).unwrap(); let mut file = std::fs::File::create("src/minified.js").unwrap(); - file.write_all(minified.as_bytes()).unwrap(); + file.write_all(js.as_bytes()).unwrap(); } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs index ae0eb4efe..6fb1fb436 100644 --- a/packages/liveview/src/element.rs +++ b/packages/liveview/src/element.rs @@ -29,7 +29,7 @@ impl RenderedElementBacking for LiveviewElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return getClientRect({});", self.id.0); let fut = self .query @@ -53,7 +53,7 @@ impl RenderedElementBacking for LiveviewElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -76,10 +76,7 @@ impl RenderedElementBacking for LiveviewElement { &self, focus: bool, ) -> std::pin::Pin>>> { - let script = format!( - "return window.interpreter.SetFocus({}, {});", - self.id.0, focus - ); + let script = format!("return setFocus({}, {});", self.id.0, focus); let fut = self.query.new_query::(&script).resolve(); diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 4ca557127..54b575263 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -28,19 +28,26 @@ class IPC { }; ws.onmessage = (message) => { - if (message.data instanceof ArrayBuffer) { + console.log(message.data); + const u8view = new Uint8Array(message.data); + const binaryFrame = u8view[0] == 1; + const messageData = message.data.slice(1) + // The first byte tells the shim if this is a binary of text frame + if (binaryFrame) { // binary frame - run_from_bytes(message.data); - } else { + run_from_bytes(messageData); + } + else { // text frame + + let decoder = new TextDecoder("utf-8"); + + // Using decode method to get string output + let str = decoder.decode(messageData); // Ignore pongs - if (message.data != "__pong__") { - const event = JSON.parse(message.data); + if (str != "__pong__") { + const event = JSON.parse(str); switch (event.type) { - case "edits": - let edits = event.data; - window.interpreter.handleEdits(edits); - break; case "query": Function("Eval", `"use strict";${event.data};`)(); break; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index a74c183df..64adfa3d1 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -6,7 +6,7 @@ use crate::{ }; use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations}; use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData}; -use dioxus_interpreter_js::Channel; +use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, SinkExt, StreamExt}; use rustc_hash::FxHashMap; use serde::Serialize; @@ -173,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(text_frame("__pong__")).await?; } Some(Ok(evt)) => { if let Ok(message) = serde_json::from_str::(&String::from_utf8_lossy(evt)) { @@ -212,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(text_frame(&serde_json::to_string(&ClientUpdate::Query(query)).unwrap())).await?; } Some(msg) = hot_reload_wait => { @@ -247,13 +247,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li } } +fn text_frame(text: &str) -> Vec { + let mut bytes = vec![0]; + bytes.extend(text.as_bytes()); + bytes +} + fn add_template( template: &Template<'static>, channel: &mut Channel, templates: &mut FxHashMap, max_template_count: &mut u16, ) { - for (idx, root) in template.roots.iter().enumerate() { + for root in template.roots.iter() { create_template_node(channel, root); templates.insert(template.name.to_owned(), *max_template_count); } @@ -368,16 +374,16 @@ fn apply_edits( } } - let bytes: Vec<_> = channel.export_memory().collect(); + // Add an extra one at the beginning to tell the shim this is a binary frame + let mut bytes = vec![1]; + bytes.extend(channel.export_memory()); channel.reset(); Some(bytes) } #[derive(Serialize)] #[serde(tag = "type", content = "data")] -enum ClientUpdate<'a> { - #[serde(rename = "edits")] - Edits(Mutations<'a>), +enum ClientUpdate { #[serde(rename = "query")] Query(String), } From 007aacc24706061128f56a72b108688281e24c17 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 19 Oct 2023 16:41:47 -0500 Subject: [PATCH 10/21] remove liveview logging --- packages/liveview/src/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 54b575263..0c60386e3 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -28,7 +28,6 @@ class IPC { }; ws.onmessage = (message) => { - console.log(message.data); const u8view = new Uint8Array(message.data); const binaryFrame = u8view[0] == 1; const messageData = message.data.slice(1) From 378cbfabd9234892f357b31dc21f972b127a4020 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Oct 2023 08:45:04 -0500 Subject: [PATCH 11/21] fix desktop mounted event --- packages/desktop/src/element.rs | 6 +- packages/desktop/src/lib.rs | 2 +- packages/interpreter/src/interpreter.js | 121 +----------------- .../interpreter/src/sledgehammer_bindings.rs | 5 +- packages/liveview/src/element.rs | 9 +- 5 files changed, 17 insertions(+), 126 deletions(-) diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index 20ca951bf..b5f12c3f2 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -30,7 +30,7 @@ impl RenderedElementBacking for DesktopElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -54,7 +54,7 @@ impl RenderedElementBacking for DesktopElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -81,7 +81,7 @@ impl RenderedElementBacking for DesktopElement { focus: bool, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.SetFocus({}, {});", + "return window.interpreter.setFocus({}, {});", self.id.0, focus ); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 406d6ffb8..b092ceeec 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -29,7 +29,7 @@ use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandl use dioxus_core::*; use dioxus_html::{event_bubbles, MountedData}; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use dioxus_interpreter_js::Channel; +use dioxus_interpreter_js::binary_protocol::Channel; use element::DesktopElement; use eval::init_eval; use futures_util::{pin_mut, FutureExt}; diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index aaeb3a871..b5f80a377 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,5 +1,3 @@ -import { setAttributeInner } from "./common.js"; - class InterpreterConfig { constructor(intercept_link_redirects) { this.intercept_link_redirects = intercept_link_redirects; @@ -215,45 +213,6 @@ class ListenerMap { 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]; @@ -284,41 +243,10 @@ function AppendChildren(id, many) { 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; -} +window.interpreter = {} -function getClientRect(id) { +window.interpreter.getClientRect = function (id) { const node = nodes[id]; if (!node) { return; @@ -331,7 +259,7 @@ function getClientRect(id) { }; } -function scrollTo(id, behavior) { +window.interpreter.scrollTo = function (id, behavior) { const node = nodes[id]; if (!node) { return false; @@ -343,7 +271,7 @@ function scrollTo(id, behavior) { } /// Set the focus on the element -function setFocus(id, focus) { +window.interpreter.setFocus = function (id, focus) { const node = nodes[id]; if (!node) { return false; @@ -356,47 +284,6 @@ function setFocus(id, focus) { 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/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index ac1fbf5f0..4203f4d15 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -296,6 +296,7 @@ pub mod binary_protocol { #[bindgen] mod protocol_js { const JS_FILE: &str = "./packages/interpreter/src/interpreter.js"; + const JS_FILE: &str = "./packages/interpreter/src/common.js"; fn mount_to_root() { "{AppendChildren(root, stack.length-1);}" @@ -382,10 +383,10 @@ pub mod binary_protocol { "{nodes[$id$].textContent = $text$;}" } fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + "{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$);}" + "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" } fn remove_attribute(id: u32, field: &str, ns: &str) { r#"{ diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs index 6fb1fb436..600f7f20d 100644 --- a/packages/liveview/src/element.rs +++ b/packages/liveview/src/element.rs @@ -29,7 +29,7 @@ impl RenderedElementBacking for LiveviewElement { >, >, > { - let script = format!("return getClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -53,7 +53,7 @@ impl RenderedElementBacking for LiveviewElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return scrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -76,7 +76,10 @@ impl RenderedElementBacking for LiveviewElement { &self, focus: bool, ) -> std::pin::Pin>>> { - let script = format!("return setFocus({}, {});", self.id.0, focus); + let script = format!( + "return window.interpreter.setFocus({}, {});", + self.id.0, focus + ); let fut = self.query.new_query::(&script).resolve(); From 2404bfeeb3a863bc6cd01105d22a9ca6c178c7ff Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Oct 2023 09:06:19 -0500 Subject: [PATCH 12/21] fix liveview element --- packages/desktop/build.rs | 4 +++- packages/liveview/build.rs | 5 +++-- packages/liveview/src/query.rs | 5 +---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index bd7445637..770ebd2d6 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -26,7 +26,9 @@ fn main() { } } }"#; - let mut interpreter = SLEDGEHAMMER_JS.replace("/*POST_HANDLE_EDITS*/", prevent_file_upload); + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) + .replace("export", ""); while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs index b62a09da0..824bc4f70 100644 --- a/packages/liveview/build.rs +++ b/packages/liveview/build.rs @@ -40,8 +40,9 @@ fn main() { return; } }"#; - let mut interpreter = - SLEDGEHAMMER_JS.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads); + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) + .replace("export", ""); while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs index a638625c1..c99dd5151 100644 --- a/packages/liveview/src/query.rs +++ b/packages/liveview/src/query.rs @@ -169,10 +169,7 @@ pub(crate) struct Query { impl Query { /// Resolve the query pub async fn resolve(mut self) -> Result { - match self.receiver.recv().await { - Some(result) => V::deserialize(result).map_err(QueryError::Deserialize), - None => Err(QueryError::Recv(RecvError::Closed)), - } + V::deserialize(self.result().await?).map_err(QueryError::Deserialize) } /// Send a message to the query From 1d664c616eada1119274358ebe3de2215bdab352 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Oct 2023 09:18:16 -0500 Subject: [PATCH 13/21] restore desktop/liveview minification --- packages/desktop/build.rs | 33 ++++++++++++------- packages/desktop/src/protocol.rs | 18 ++-------- packages/interpreter/src/interpreter.js | 20 ++++++----- .../interpreter/src/sledgehammer_bindings.rs | 2 +- packages/liveview/build.rs | 10 +++--- packages/liveview/src/main.js | 4 +-- 6 files changed, 43 insertions(+), 44 deletions(-) diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index 770ebd2d6..8be7761c0 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -16,7 +16,7 @@ fn main() { let target_id = find_real_id(target); if (target_id !== null) { const send = (event_name) => { - const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); window.ipc.postMessage(message); }; send("change&input"); @@ -26,9 +26,21 @@ fn main() { } } }"#; + let polling_request = r#"// Poll for requests + window.interpreter.wait_for_request = () => { + fetch(new Request("dioxus://index.html/edits")) + .then(response => { + response.arrayBuffer() + .then(bytes => { + run_from_bytes(bytes); + window.interpreter.wait_for_request(); + }); + }) + }"#; let mut interpreter = SLEDGEHAMMER_JS .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) - .replace("export", ""); + .replace("export", "") + + polling_request; while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') @@ -38,15 +50,12 @@ fn main() { } let js = format!("{interpreter}\nconst config = new InterpreterConfig(false);"); - let mut file = std::fs::File::create("src/minified.js").unwrap(); - file.write_all(js.as_bytes()).unwrap(); - // TODO: Enable minification on desktop - // use minify_js::*; - // 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(); - // let mut file = std::fs::File::create("src/minified.js").unwrap(); - // file.write_all(minified.as_bytes()).unwrap(); + use minify_js::*; + 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(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index f8ba940f4..41a491749 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -17,27 +17,15 @@ fn module_loader(root_name: &str) -> String { r#" "# diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b5f80a377..54f1d517c 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -35,7 +35,7 @@ function handler(event, name, bubbles, config) { const href = a_element.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) + window.interpreter.serializeIpcMessage("browser_open", { href }) ); } } @@ -89,7 +89,7 @@ function handler(event, name, bubbles, config) { if (realId === null) { return; } - const message = serializeIpcMessage("user_event", { + const message = window.interpreter.serializeIpcMessage("user_event", { name: name, element: parseInt(realId), data: contents, @@ -132,7 +132,7 @@ function handler(event, name, bubbles, config) { return; } window.ipc.postMessage( - serializeIpcMessage("user_event", { + window.interpreter.serializeIpcMessage("user_event", { name: name, element: parseInt(realId), data: contents, @@ -231,11 +231,7 @@ 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); @@ -246,6 +242,12 @@ function AppendChildren(id, many) { window.interpreter = {} +window.interpreter.initialize = function (root) { + nodes = [root]; + stack = [root]; + listeners.root = root; +} + window.interpreter.getClientRect = function (id) { const node = nodes[id]; if (!node) { @@ -560,7 +562,7 @@ function serialize_event(event) { } } } -function serializeIpcMessage(method, params = {}) { +window.interpreter.serializeIpcMessage = function (method, params = {}) { return JSON.stringify({ method, params }); } diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 4203f4d15..e0d4f7045 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -363,7 +363,7 @@ pub mod binary_protocol { // if this is a mounted listener, we send the event immediately if (event_name === "mounted") { window.ipc.postMessage( - serializeIpcMessage("user_event", { + window.interpreter.serializeIpcMessage("user_event", { name: event_name, element: id, data: null, diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs index 824bc4f70..d4dbeb09a 100644 --- a/packages/liveview/build.rs +++ b/packages/liveview/build.rs @@ -28,7 +28,7 @@ fn main() { if (realId === null) { return; } - const message = serializeIpcMessage("user_event", { + const message = window.interpreter.serializeIpcMessage("user_event", { name: name, element: parseInt(realId), data: contents, @@ -56,9 +56,9 @@ fn main() { let js = format!("{interpreter}\n{main_js}"); 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(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); let mut file = std::fs::File::create("src/minified.js").unwrap(); - file.write_all(js.as_bytes()).unwrap(); + file.write_all(minified.as_bytes()).unwrap(); } diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 0c60386e3..22267dbab 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -9,7 +9,7 @@ function main() { class IPC { constructor(root) { - initialize(root); + window.interpreter.initialize(root); const ws = new WebSocket(WS_ADDR); ws.binaryType = "arraybuffer"; @@ -20,7 +20,7 @@ class IPC { ws.onopen = () => { // we ping every 30 seconds to keep the websocket alive setInterval(ping, 30000); - ws.send(serializeIpcMessage("initialize")); + ws.send(window.interpreter.serializeIpcMessage("initialize")); }; ws.onerror = (err) => { From 33f0f0c17206b02424562f192e7436ddaae2dc1c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Oct 2023 09:38:10 -0500 Subject: [PATCH 14/21] fix clippy --- packages/cli/src/builder.rs | 2 +- packages/core/src/scope_context.rs | 2 +- packages/desktop/src/lib.rs | 2 +- packages/desktop/src/protocol.rs | 3 +-- packages/interpreter/Cargo.toml | 2 +- packages/liveview/src/pool.rs | 2 +- packages/server-macro/src/lib.rs | 2 +- packages/web/Cargo.toml | 1 - 8 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index c4d1bde6f..fd3202d2e 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -312,7 +312,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result() }) { return Some(shared.clone()); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index b092ceeec..4780690ee 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -484,7 +484,7 @@ fn apply_edits( 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) + channel.load_template(*tmpl_id, index as u16, id.0 as u32) } } ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 41a491749..1dca21759 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -126,7 +126,6 @@ pub(super) fn desktop_handler( { Ok(response) => { responder.respond(response); - return; } Err(err) => tracing::error!("error building response: {}", err), } @@ -167,7 +166,7 @@ fn get_asset_root() -> Option { /// Get the mime type from a path-like string fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { - if trimmed.ends_with(".svg") { + if trimmed.extension().is_some_and(|ext| ext == "svg") { return Ok("image/svg+xml"); } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 8d155db6d..7d046fbc8 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -23,5 +23,5 @@ default = [] serialize = ["serde"] sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"] web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"] -binary-protocol = ["sledgehammer"] +binary-protocol = ["sledgehammer", "wasm-bindgen"] minimal_bindings = [] diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 64adfa3d1..eaf699248 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -329,7 +329,7 @@ fn apply_edits( 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) + channel.load_template(*tmpl_id, index as u16, id.0 as u32) } } ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index d00c009d0..2afa19742 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -105,7 +105,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { let upper_cammel_case_name = Converter::new() .from_case(Case::Snake) .to_case(Case::UpperCamel) - .convert(&sig.ident.to_string()); + .convert(sig.ident.to_string()); args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span())); } let struct_name = args.struct_name.as_ref().unwrap(); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 428815576..a21acded7 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -13,7 +13,6 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["wasm-bind"] } dioxus-interpreter-js = { workspace = true, features = [ - "sledgehammer", "minimal_bindings", "web", ] } From b14aaca7b2ed9c5df91dc2a907a05924c23dd844 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Oct 2023 13:05:13 -0500 Subject: [PATCH 15/21] fix desktop renderer on windows --- packages/desktop/build.rs | 31 ++++++++++++++++++++++--------- packages/desktop/src/webview.rs | 2 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index 8be7761c0..b7989c595 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -2,6 +2,17 @@ use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; use std::io::Write; +const EDITS_PATH: &str = { + #[cfg(any(target_os = "android", target_os = "windows"))] + { + "http://dioxus.index.html/edits" + } + #[cfg(not(any(target_os = "android", target_os = "windows")))] + { + "dioxus://index.html/edits" + } +}; + fn main() { let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click let inputs = document.querySelectorAll("input"); @@ -26,21 +37,23 @@ fn main() { } } }"#; - let polling_request = r#"// Poll for requests - window.interpreter.wait_for_request = () => { - fetch(new Request("dioxus://index.html/edits")) - .then(response => { + let polling_request = format!( + r#"// Poll for requests + window.interpreter.wait_for_request = () => {{ + fetch(new Request("{EDITS_PATH}")) + .then(response => {{ response.arrayBuffer() - .then(bytes => { + .then(bytes => {{ run_from_bytes(bytes); window.interpreter.wait_for_request(); - }); - }) - }"#; + }}); + }}) + }}"# + ); let mut interpreter = SLEDGEHAMMER_JS .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) .replace("export", "") - + polling_request; + + &polling_request; while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] .find(|c| c == ';' || c == '\n') diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 44b5a3bf0..0992363e6 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -7,7 +7,7 @@ pub use wry::application as tao; use wry::application::window::Window; use wry::webview::{WebContext, WebView, WebViewBuilder}; -pub fn build( +pub(crate) fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, From 9de56c00f305a71cc25e491dad0e4a1f5ef94616 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 24 Oct 2023 13:09:10 -0500 Subject: [PATCH 16/21] fix multiple types of desktop/liveview listeners in the same app --- packages/desktop/src/lib.rs | 5 ++++- packages/interpreter/src/sledgehammer_bindings.rs | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 4780690ee..c67ecbc80 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -276,7 +276,10 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let evt = match serde_json::from_value::(params) { Ok(value) => value, - Err(_) => return, + Err(err) => { + tracing::error!("Error parsing user_event: {:?}", err); + return; + } }; let HtmlEvent { diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 981197787..fc66fb5a8 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -355,7 +355,7 @@ pub mod binary_protocol { fn add_placeholder() { "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" } - fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { + fn new_event_listener(event: &str, id: u32, bubbles: u8) { r#" bubbles = bubbles == 1; node = nodes[id]; @@ -365,6 +365,7 @@ pub mod binary_protocol { node.listening = 1; } node.setAttribute('data-dioxus-id', `\${id}`); + const event_name = $event$; // if this is a mounted listener, we send the event immediately if (event_name === "mounted") { From dc306a51ba8479a8f93fbe3f22d57021e3165698 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 24 Oct 2023 13:17:34 -0500 Subject: [PATCH 17/21] fix desktop inner html --- packages/interpreter/src/common.js | 2 +- .../interpreter/src/sledgehammer_bindings.rs | 46 +------------------ 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js index 3745e8c6b..381939fb8 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.js @@ -36,7 +36,7 @@ export function setAttributeInner(node, field, value, ns) { node.style = {}; } node.style[name] = value; - } else if (ns != null && ns != undefined) { + } else if (!!ns) { node.setAttributeNS(ns, name, value); } else { switch (name) { diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index fc66fb5a8..54162fc8a 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -11,6 +11,7 @@ pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; #[cfg(feature = "web")] #[bindgen(module)] mod js { + const JS_FILE: &str = "./packages/interpreter/src/common.js"; const JS: &str = r#" class ListenerMap { constructor(root) { @@ -64,51 +65,6 @@ mod js { 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 "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: - // 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(ptr, len) { // iterate through each number and get that child node = stack[stack.length - 1]; From 3bb6042e42bdf1e07c7f2364a54347cf59153e41 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 25 Oct 2023 10:22:05 -0500 Subject: [PATCH 18/21] fix web renderer imports --- packages/interpreter/src/common.js | 60 +++++++++---------- .../interpreter/src/sledgehammer_bindings.rs | 34 +---------- 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js index 381939fb8..0c64085f9 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.js @@ -1,33 +1,3 @@ -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, -}; - export function setAttributeInner(node, field, value, ns) { const name = field; if (ns === "style") { @@ -74,6 +44,36 @@ export function setAttributeInner(node, field, value, ns) { } } +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; } diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 54162fc8a..00b6d8ca0 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -105,38 +105,6 @@ mod js { 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; - } "#; extern "C" { @@ -196,7 +164,7 @@ mod js { "{nodes[$id$].textContent = $text$;}" } fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" } fn remove_attribute(id: u32, field: &str, ns: &str) { r#"{ From 71f7481dedf6590e91b03a12df7b1f5cfd447bab Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 4 Dec 2023 20:45:26 -0600 Subject: [PATCH 19/21] use requestAnimationFrame on desktop --- packages/desktop/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index b7989c595..795cc2ace 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -44,7 +44,9 @@ fn main() { .then(response => {{ response.arrayBuffer() .then(bytes => {{ - run_from_bytes(bytes); + requestAnimationFrame(() => {{ + run_from_bytes(bytes); + }}); window.interpreter.wait_for_request(); }}); }}) From 8984482b631e623ea5c022496c9e030db13f50f0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 8 Dec 2023 15:14:32 -0600 Subject: [PATCH 20/21] fix headless windows --- packages/desktop/build.rs | 14 ++++++++++---- packages/desktop/headless_tests/events.rs | 2 +- packages/desktop/src/protocol.rs | 9 +++++---- packages/desktop/src/webview.rs | 2 ++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index f114ae084..8d1f4b3e5 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -51,15 +51,21 @@ fn main() { }"#; let polling_request = format!( r#"// Poll for requests - window.interpreter.wait_for_request = () => {{ + window.interpreter.wait_for_request = (headless) => {{ fetch(new Request("{EDITS_PATH}")) .then(response => {{ response.arrayBuffer() .then(bytes => {{ - requestAnimationFrame(() => {{ + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) {{ run_from_bytes(bytes); - }}); - window.interpreter.wait_for_request(); + }} + else {{ + requestAnimationFrame(() => {{ + run_from_bytes(bytes); + }}); + }} + window.interpreter.wait_for_request(headless); }}); }}) }}"# diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index bb3b46206..646b951e1 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -17,7 +17,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(true)), + Config::new().with_window(WindowBuilder::new().with_visible(false)), ); // Stop deadman's switch diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 1dca21759..8184821ba 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -12,7 +12,7 @@ use crate::desktop_context::EditQueue; static MINIFIED: &str = include_str!("./minified.js"); -fn module_loader(root_name: &str) -> String { +fn module_loader(root_name: &str, headless: bool) -> String { format!( r#" "# @@ -39,6 +39,7 @@ pub(super) fn desktop_handler( custom_index: Option, root_name: &str, edit_queue: &EditQueue, + headless: bool, ) { // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { @@ -46,7 +47,7 @@ pub(super) fn desktop_handler( // we'll look for the closing tag and insert our little module loader there. let body = match custom_index { Some(custom_index) => custom_index - .replace("", &format!("{}", module_loader(root_name))) + .replace("", &format!("{}", module_loader(root_name, headless))) .into_bytes(), None => { @@ -58,7 +59,7 @@ pub(super) fn desktop_handler( } template - .replace("", &module_loader(root_name)) + .replace("", &module_loader(root_name, headless)) .into_bytes() } }; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 426cc8706..d921a880f 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -33,6 +33,7 @@ pub(crate) fn build( let mut web_context = WebContext::new(cfg.data_dir.clone()); let edit_queue = EditQueue::default(); + let headless = !cfg.window.window.visible; let mut webview = WebViewBuilder::new(window) .unwrap() @@ -55,6 +56,7 @@ pub(crate) fn build( index_file.clone(), &root_name, &edit_queue, + headless ) } }) From 3afb3131106506ae0cf42cb384dfb813912bc9e2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 11 Dec 2023 14:00:45 -0600 Subject: [PATCH 21/21] fix formatting --- packages/desktop/src/protocol.rs | 10 ++++++++-- packages/desktop/src/webview.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 8184821ba..d8ed0478c 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -47,7 +47,10 @@ pub(super) fn desktop_handler( // we'll look for the closing tag and insert our little module loader there. let body = match custom_index { Some(custom_index) => custom_index - .replace("", &format!("{}", module_loader(root_name, headless))) + .replace( + "", + &format!("{}", module_loader(root_name, headless)), + ) .into_bytes(), None => { @@ -59,7 +62,10 @@ pub(super) fn desktop_handler( } template - .replace("", &module_loader(root_name, headless)) + .replace( + "", + &module_loader(root_name, headless), + ) .into_bytes() } }; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index d921a880f..216c7a1a8 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -56,7 +56,7 @@ pub(crate) fn build( index_file.clone(), &root_name, &edit_queue, - headless + headless, ) } })