diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index a67432e98..c17a63d2a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -12,7 +12,9 @@
- + + + + diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js deleted file mode 100644 index 2aba572f4..000000000 --- a/packages/desktop/src/index.js +++ /dev/null @@ -1,468 +0,0 @@ -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - - case "compositionend": - case "compositionstart": - case "compositionupdate": - return { - data: event.data, - }; - - case "keydown": - case "keypress": - case "keyup": - return { - char_code: event.charCode, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - locale: "locale", - location: event.location, - repeat: event.repeat, - which: event.which, - // locale: event.locale, - }; - - 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, - }; - - 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, - }; - } - - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - - case "select": - return {}; - - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - }; - - case "scroll": - return {}; - - case "wheel": - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - - case "animationstart": - case "animationend": - case "animationiteration": - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "transitionend": - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.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 {}; - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = { - onclick: {}, - }; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(edit) { - const id = edit.root; - const node = this.nodes[id]; - this.stack.push(node); - } - - AppendChildren(edit) { - let root = this.stack[this.stack.length - (1 + edit.many)]; - - let to_add = this.stack.splice(this.stack.length - edit.many); - - for (let i = 0; i < edit.many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(edit) { - let root = this.nodes[edit.root]; - let els = this.stack.splice(this.stack.length - edit.m); - - root.replaceWith(...els); - } - - InsertAfter(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.after(...new_nodes); - } - - InsertBefore(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.before(...new_nodes); - } - - Remove(edit) { - let node = this.nodes[edit.root]; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(edit) { - const node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; - this.stack.push(node); - } - - CreateElement(edit) { - const tagName = edit.tag; - const el = document.createElement(tagName); - this.nodes[edit.root] = el; - el.setAttribute("dioxus-id", edit.root); - this.stack.push(el); - } - - CreateElementNs(edit) { - let el = document.createElementNS(edit.ns, edit.tag); - this.stack.push(el); - this.nodes[edit.root] = el; - } - - CreatePlaceholder(edit) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[edit.root] = el; - } - - RemoveEventListener(edit) {} - - NewEventListener(edit) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - - const element = this.nodes[edit.root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = true; - - this.root.addEventListener(event_name, (event) => { - - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (should_prevent_default !== `onclick`) { - if(element.tagName == "A") { - const href = event.target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined) { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: event.target.getAttribute("href") - }); - } - } - } - } - - if (real_id == null) { - return; - } - - rpc.call("user_event", { - event: event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - }); - } - } - - SetText(edit) { - this.nodes[edit.root].textContent = edit.text; - } - - SetAttribute(edit) { - - // console.log("setting attr", edit); - - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; - - if (ns == "style") { - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(edit) { - const name = edit.field; - const node = this.nodes[edit.root]; - node.removeAttribute(name); - - if (name === "value") { - node.value = null; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - - handleEdits(edits) { - this.stack.push(this.root); - - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = this[edit.type]; - f.call(this, edit); - } - } -} - -function main() { - let root = window.document.getElementById("main"); - window.interpreter = new Interpreter(root); - rpc.call("initialize"); -} - -main(); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 8526a4a93..802e7441c 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -217,7 +217,7 @@ pub fn launch_with_props( } else if path.trim_end_matches('/') == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("./index.js").to_vec()) + .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::NOT_FOUND) diff --git a/packages/jsinterpreter/README.md b/packages/jsinterpreter/README.md new file mode 100644 index 000000000..bab57ac16 --- /dev/null +++ b/packages/jsinterpreter/README.md @@ -0,0 +1,8 @@ +# JS Interpreter + +After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support. + +In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. + + +For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js new file mode 100644 index 000000000..453b34406 --- /dev/null +++ b/packages/jsinterpreter/interpreter.js @@ -0,0 +1,477 @@ +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + var data = event.data; + return { + data: data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + var target = event.target; + var value = (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey; + 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_1, + screen_y: screenY_1, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; + 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_2, + screen_y: screenY_2, + 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": { + var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey; + 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": { + var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement; + 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 {}; + } + } +} +var 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, +}; +var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (text, root) { + // todo: make it so the types are okay + var node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (root) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) { + // console.log('new event listener', event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + }; + Interpreter.prototype.RemoveEventListener = function (root, event_name) { + var element = this.nodes[root]; + element.removeAttribute("dioxus-event-".concat(event_name)); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + }; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + this.stack.push(this.root); + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + // console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + }; + return Interpreter; +}()); +export { Interpreter }; +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +//# sourceMappingURL=interpreter.js.map \ No newline at end of file diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts new file mode 100644 index 000000000..533536277 --- /dev/null +++ b/packages/jsinterpreter/interpreter.ts @@ -0,0 +1,676 @@ +function serialize_event(event: Event) { + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = (event as CompositionEvent); + return { + data, + }; + } + + case "keydown": + case "keypress": + case "keyup": { + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = (event as KeyboardEvent); + + 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, + locale: "locale", + }; + } + + case "focus": + case "blur": { + return {}; + } + + case "change": { + let target = event.target as HTMLInputElement; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + + return { + value: value, + }; + } + + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target as HTMLFormElement; + let value = target.value ?? target.textContent; + + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + + return { + value: value, + }; + } + + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + } = event as MouseEvent; + + 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, + }; + } + + 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 as PointerEvent; + 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 as TouchEvent; + 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 as WheelEvent; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + + case "animationstart": + case "animationend": + case "animationiteration": { + const { + animationName, + elapsedTime, + pseudoElement, + } = event as AnimationEvent; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + + case "transitionend": { + const { + propertyName, + elapsedTime, + pseudoElement, + } = event as TransitionEvent; + 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 {}; + } + } +} + +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, +}; + +export class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(root: number) { + const node = this.nodes[root]; + this.stack.push(node); + } + + AppendChildren(many: number) { + 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: number, m: number) { + let root = this.nodes[root_id] as Element; + let els = this.stack.splice(this.stack.length - m); + + root.replaceWith(...els); + } + + InsertAfter(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + + InsertBefore(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + + Remove(root: number) { + let node = this.nodes[root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(text: string, root: number) { + // todo: make it so the types are okay + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; + this.stack.push(node); + } + + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + el.setAttribute("dioxus-id", `${root}`); + + this.nodes[root] = el; + this.stack.push(el); + } + + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + + CreatePlaceholder(root: number) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + + NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) { + // console.log('new event listener', event_name, root, scope); + const element = this.nodes[root]; + element.setAttribute( + `dioxus-event-${event_name}`, + `${scope}.${root}` + ); + + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + + RemoveEventListener(root: number, event_name: string) { + const element = this.nodes[root]; + element.removeAttribute( + `dioxus-event-${event_name}` + ); + + this.listeners[event_name]--; + + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + + + SetText(root: number, text: string) { + this.nodes[root].textContent = text; + } + + SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + const name = field; + const node = this.nodes[root]; + + if (ns == "style") { + + // @ts-ignore + (node as HTMLElement).style[name] = value; + + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != (node as HTMLInputElement).value) { + (node as HTMLInputElement).value = value; + } + break; + case "checked": + (node as HTMLInputElement).checked = value === "true"; + break; + case "selected": + (node as HTMLOptionElement).selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root: number, name: string) { + + const node = this.nodes[root]; + node.removeAttribute(name); + + if (name === "value") { + (node as HTMLInputElement).value = ""; + } + + if (name === "checked") { + (node as HTMLInputElement).checked = false; + } + + if (name === "selected") { + (node as HTMLOptionElement).selected = false; + } + } + + handleEdits(edits: DomEdit[]) { + this.stack.push(this.root); + + for (let edit of edits) { + this.handleEdit(edit); + } + } + + handleEdit(edit: DomEdit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event: Event) => { + const target = event.target as Element | null; + + if (target != null) { + const realId = target.getAttribute(`dioxus-id`); + + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + + let contents = serialize_event(event); + + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} + +function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + + +type PushRoot = { type: "PushRoot", root: number }; +type AppendChildren = { type: "AppendChildren", many: number }; +type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; +type InsertAfter = { type: "InsertAfter", root: number, n: number }; +type InsertBefore = { type: "InsertBefore", root: number, n: number }; +type Remove = { type: "Remove", root: number }; +type CreateTextNode = { type: "CreateTextNode", text: string, root: number }; +type CreateElement = { type: "CreateElement", tag: string, root: number }; +type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string }; +type CreatePlaceholder = { type: "CreatePlaceholder", root: number }; +type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number }; +type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number }; +type SetText = { type: "SetText", root: number, text: string }; +type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; +type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; + + +type DomEdit = + PushRoot | + AppendChildren | + ReplaceWith | + InsertAfter | + InsertBefore | + Remove | + CreateTextNode | + CreateElement | + CreateElementNs | + CreatePlaceholder | + NewEventListener | + RemoveEventListener | + SetText | + SetAttribute | + RemoveAttribute; + + +export { }; +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json new file mode 100644 index 000000000..883760881 --- /dev/null +++ b/packages/jsinterpreter/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES5", + "module": "ESNext", + "lib": [ + "es2015", + "es5", + "es6", + "dom" + ], + "strict": true + } +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 868c5d7b0..52a78e631 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} -dioxus-html = { path = "../html", version ="^0.1.4"} +dioxus-core = { path = "../core", version = "^0.1.7" } +dioxus-html = { path = "../html", version = "^0.1.4" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs new file mode 100644 index 000000000..9b0c2ebfb --- /dev/null +++ b/packages/web/src/bindings.rs @@ -0,0 +1,59 @@ +use js_sys::Function; +use wasm_bindgen::prelude::*; +use web_sys::{Element, Node}; + +#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] +extern "C" { + pub type Interpreter; + + #[wasm_bindgen(constructor)] + pub fn new(arg: Element) -> Interpreter; + + #[wasm_bindgen(method)] + pub fn set_node(this: &Interpreter, id: usize, node: Node); + + #[wasm_bindgen(method)] + pub fn PushRoot(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, many: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn Remove(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); + + #[wasm_bindgen(method)] + pub fn CreatePlaceholder(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); + + #[wasm_bindgen(method)] + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, root: u64, text: &str); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); +} diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index 190b505c1..e17203a65 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -1,4 +1,4 @@ -pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[ +pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ // Important tags to dioxus "dioxus-id", "dioxus", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 0fa0dc48c..cc5d18bf7 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -3,10 +3,9 @@ /// This struct helps configure the specifics of hydration and render destination for WebSys. /// /// # Example +/// /// ```rust, ignore -/// fn main() { -/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) -/// } +/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) /// ``` pub struct WebConfig { pub(crate) hydrate: bool, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 219f5aa79..b0b4dd035 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,56 +7,51 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use fxhash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; +use crate::bindings::Interpreter; +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use js_sys::Function; +use std::{any::Any, rc::Rc, sync::Arc}; use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; +use web_sys::{Document, Element, Event, HtmlElement}; -use crate::{nodeslab::NodeSlab, WebConfig}; +use crate::WebConfig; pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - - document: Document, + pub interpreter: Interpreter, pub(crate) root: Element, - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, + pub handler: Closure, } -type ListenerEntry = (usize, Closure); - impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { - let document = load_document(); + // eventually, we just want to let the interpreter do all the work of decoding events into our event type + let callback: Box = Box::new(move |event: &web_sys::Event| { + if let Ok(synthetic_event) = decode_trigger(event) { + // Try to prevent default if the attribute is set + if let Some(target) = event.target() { + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + } - let nodes = NodeSlab::new(2000); - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); + sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + }); let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); Self { - stack, - nodes, - listeners, - document, - sender_callback, + interpreter: Interpreter::new(root.clone()), + handler: Closure::wrap(callback), root, } } @@ -64,434 +59,49 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { for edit in edits.drain(..) { match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) + DomEdit::PushRoot { root } => self.interpreter.PushRoot(root), + DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many), + DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m), + DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n), + DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n), + DomEdit::Remove { root } => self.interpreter.Remove(root), + DomEdit::CreateTextNode { text, root } => { + self.interpreter.CreateTextNode(text, root) } - DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), + DomEdit::CreateElementNs { tag, root, ns } => { + self.interpreter.CreateElementNs(tag, root, ns) + } + DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), DomEdit::NewEventListener { event_name, root, .. - } => self.new_event_listener(event_name, root), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) + } => { + let handler: &Function = self.handler.as_ref().unchecked_ref(); + self.interpreter.NewEventListener(event_name, root, handler); } - DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::RemoveEventListener { root, event } => { + self.interpreter.RemoveEventListener(root, event) + } + DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), DomEdit::SetAttribute { + root, field, value, ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); - } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else if let Some(parent) = node.parent_node() { - parent.remove_child(node).unwrap(); - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - pub fn new_event_listener(&mut self, event: &'static str, node: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.nodes[(node as usize)].as_ref().unwrap(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event, (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } + } => self.interpreter.SetAttribute(root, field, value, ns), + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) } } } } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } - } - } -} - -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } } pub struct DioxusWebsysEvent(web_sys::Event); // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +#[allow(clippy::non_send_fields_in_send_ty)] unsafe impl Send for DioxusWebsysEvent {} unsafe impl Sync for DioxusWebsysEvent {} @@ -628,6 +238,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(()), "wheel" => { let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 67d52f4fe..8c0c0c3b4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,10 +61,10 @@ pub use dioxus_core as dioxus; use dioxus_core::prelude::Component; use futures_util::FutureExt; +pub(crate) mod bindings; mod cache; mod cfg; mod dom; -mod nodeslab; mod rehydrate; mod ric_raf; diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs deleted file mode 100644 index b986bd34e..000000000 --- a/packages/web/src/nodeslab.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs new file mode 100644 index 000000000..8654ced90 --- /dev/null +++ b/packages/web/src/olddom.rs @@ -0,0 +1,857 @@ +//! Implementation of a renderer for Dioxus on the web. +//! +//! Oustanding todos: +//! - Removing event listeners (delegation) +//! - Passive event listeners +//! - no-op event listener patch for safari +//! - tests to ensure dyn_into works for various event types. +//! - Partial delegation?> + +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; +use fxhash::FxHashMap; +use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, HtmlTextAreaElement, Node, +}; + +use crate::{nodeslab::NodeSlab, WebConfig}; + +pub struct WebsysDom { + stack: Stack, + + /// A map from ElementID (index) to Node + pub(crate) nodes: NodeSlab, + + document: Document, + + pub(crate) root: Element, + + sender_callback: Rc, + + // map of listener types to number of those listeners + // This is roughly a delegater + // TODO: check how infero delegates its events - some are more performant + listeners: FxHashMap<&'static str, ListenerEntry>, +} + +type ListenerEntry = (usize, Closure); + +impl WebsysDom { + pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { + let document = load_document(); + + let nodes = NodeSlab::new(2000); + let listeners = FxHashMap::default(); + + let mut stack = Stack::with_capacity(10); + + let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let root_node = root.clone().dyn_into::().unwrap(); + stack.push(root_node); + + Self { + stack, + nodes, + listeners, + document, + sender_callback, + root, + } + } + + pub fn apply_edits(&mut self, mut edits: Vec) { + for edit in edits.drain(..) { + match edit { + DomEdit::PushRoot { root } => self.push(root), + DomEdit::AppendChildren { many } => self.append_children(many), + DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), + DomEdit::Remove { root } => self.remove(root), + DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), + DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), + DomEdit::CreateElementNs { tag, root: id, ns } => { + self.create_element(tag, Some(ns), id) + } + DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::NewEventListener { + event_name, + scope, + root: mounted_node_id, + } => self.new_event_listener(event_name, scope, mounted_node_id), + + DomEdit::RemoveEventListener { event, root } => { + self.remove_event_listener(event, root) + } + + DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetAttribute { + field, + value, + ns, + root, + } => self.set_attribute(field, value, ns, root), + DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), + + DomEdit::InsertAfter { n, root } => self.insert_after(n, root), + DomEdit::InsertBefore { n, root } => self.insert_before(n, root), + } + } + } + fn push(&mut self, root: u64) { + let key = root as usize; + let domnode = &self.nodes[key]; + + let real_node: Node = match domnode { + Some(n) => n.clone(), + None => todo!(), + }; + + self.stack.push(real_node); + } + + fn append_children(&mut self, many: u32) { + let root: Node = self + .stack + .list + .get(self.stack.list.len() - (1 + many as usize)) + .unwrap() + .clone(); + + // We need to make sure to add comments between text nodes + // We ensure that the text siblings are patched by preventing the browser from merging + // neighboring text nodes. Originally inspired by some of React's work from 2016. + // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes + // -> https://github.com/facebook/react/pull/5753 + /* + todo: we need to track this for replacing/insert after/etc + */ + let mut last_node_was_text = false; + + for child in self + .stack + .list + .drain((self.stack.list.len() - many as usize)..) + { + if child.dyn_ref::().is_some() { + if last_node_was_text { + let comment_node = self + .document + .create_comment("dioxus") + .dyn_into::() + .unwrap(); + root.append_child(&comment_node).unwrap(); + } + last_node_was_text = true; + } else { + last_node_was_text = false; + } + root.append_child(&child).unwrap(); + } + } + + fn replace_with(&mut self, m: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - m as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } + } + + fn remove(&mut self, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(element) = node.dyn_ref::() { + element.remove(); + } else { + if let Some(parent) = node.parent_node() { + parent.remove_child(&node).unwrap(); + } + } + } + + fn create_placeholder(&mut self, id: u64) { + self.create_element("pre", None, id); + self.set_attribute("hidden", "", None, id); + } + + fn create_text_node(&mut self, text: &str, id: u64) { + let textnode = self + .document + .create_text_node(text) + .dyn_into::() + .unwrap(); + + self.stack.push(textnode.clone()); + + self.nodes[(id as usize)] = Some(textnode); + } + + fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { + let tag = wasm_bindgen::intern(tag); + + let el = match ns { + Some(ns) => self + .document + .create_element_ns(Some(ns), tag) + .unwrap() + .dyn_into::() + .unwrap(), + None => self + .document + .create_element(tag) + .unwrap() + .dyn_into::() + .unwrap(), + }; + + use smallstr::SmallString; + use std::fmt::Write; + + let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); + write!(s, "{}", id).unwrap(); + + let el2 = el.dyn_ref::().unwrap(); + el2.set_attribute("dioxus-id", s.as_str()).unwrap(); + + self.stack.push(el.clone()); + self.nodes[(id as usize)] = Some(el); + } + + fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + let event = wasm_bindgen::intern(event); + + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. + + let el = self.stack.top(); + + let el = el.dyn_ref::().unwrap(); + + el.set_attribute("dioxus-event", event).unwrap(); + + // Register the callback to decode + + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.sender_callback.clone(); + + let c: Box = Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + match decode_trigger(event) { + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), + }; + }); + + let handler = Closure::wrap(c); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } + } + + fn remove_event_listener(&mut self, _event: &str, _root: u64) { + todo!() + } + + fn set_text(&mut self, text: &str, root: u64) { + let el = self.nodes[root as usize].as_ref().unwrap(); + el.set_text_content(Some(text)) + } + + fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if ns == Some("style") { + if let Some(el) = node.dyn_ref::() { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); + } + } else { + let fallback = || { + let el = node.dyn_ref::().unwrap(); + el.set_attribute(name, value).unwrap() + }; + match name { + "dangerous_inner_html" => { + if let Some(el) = node.dyn_ref::() { + el.set_inner_html(value); + } + } + "value" => { + if let Some(input) = node.dyn_ref::() { + /* + if the attribute being set is the same as the value of the input, then don't bother setting it. + This is used in controlled components to keep the cursor in the right spot. + + this logic should be moved into the virtualdom since we have the notion of "volatile" + */ + if input.value() != value { + input.set_value(value); + } + } else if let Some(node) = node.dyn_ref::() { + if name == "value" { + node.set_value(value); + } + } else { + fallback(); + } + } + "checked" => { + if let Some(input) = node.dyn_ref::() { + match value { + "true" => input.set_checked(true), + "false" => input.set_checked(false), + _ => fallback(), + } + } else { + fallback(); + } + } + "selected" => { + if let Some(node) = node.dyn_ref::() { + node.set_selected(true); + } else { + fallback(); + } + } + _ => { + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if value == "false" { + if let Some(el) = node.dyn_ref::() { + match name { + "allowfullscreen" + | "allowpaymentrequest" + | "async" + | "autofocus" + | "autoplay" + | "checked" + | "controls" + | "default" + | "defer" + | "disabled" + | "formnovalidate" + | "hidden" + | "ismap" + | "itemscope" + | "loop" + | "multiple" + | "muted" + | "nomodule" + | "novalidate" + | "open" + | "playsinline" + | "readonly" + | "required" + | "reversed" + | "selected" + | "truespeed" => { + let _ = el.remove_attribute(name); + } + _ => { + let _ = el.set_attribute(name, value); + } + }; + } + } else { + fallback(); + } + } + } + } + } + + fn remove_attribute(&mut self, name: &str, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + // Some attributes are "volatile" and don't work through `removeAttribute`. + if name == "value" { + node.set_value(""); + } + if name == "checked" { + node.set_checked(false); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); + } + } + } + + fn insert_after(&mut self, n: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } + } + + fn insert_before(&mut self, n: u32, root: u64) { + let anchor = self.nodes[root as usize].as_ref().unwrap(); + + if n == 1 { + let before = self.stack.pop(); + + anchor + .parent_node() + .unwrap() + .insert_before(&before, Some(&anchor)) + .unwrap(); + } else { + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } + } + } +} + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +pub struct DioxusWebsysEvent(web_sys::Event); + +// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +unsafe impl Send for DioxusWebsysEvent {} +unsafe impl Sync for DioxusWebsysEvent {} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { + use dioxus_html::on::*; + use dioxus_html::KeyCode; + + match event.type_().as_str() { + "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Arc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) + } + "keydown" | "keypress" | "keyup" => { + let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); + Arc::new(KeyboardData { + alt_key: evt.alt_key(), + char_code: evt.char_code(), + key: evt.key(), + key_code: KeyCode::from_raw_code(evt.key_code() as u8), + ctrl_key: evt.ctrl_key(), + locale: "not implemented".to_string(), + location: evt.location() as usize, + meta_key: evt.meta_key(), + repeat: evt.repeat(), + shift_key: evt.shift_key(), + which: evt.which() as usize, + }) + } + "focus" | "blur" => Arc::new(FocusData {}), + + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + "change" | "input" | "invalid" | "reset" | "submit" => { + let evt: &web_sys::Event = event.dyn_ref().unwrap(); + + let target: web_sys::EventTarget = evt.target().unwrap(); + let value: String = (&target) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + Arc::new(FormData { value }) + } + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); + Arc::new(MouseData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + }) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); + Arc::new(PointerData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + pointer_id: evt.pointer_id(), + width: evt.width(), + height: evt.height(), + pressure: evt.pressure(), + tangential_pressure: evt.tangential_pressure(), + tilt_x: evt.tilt_x(), + tilt_y: evt.tilt_y(), + twist: evt.twist(), + pointer_type: evt.pointer_type(), + is_primary: evt.is_primary(), + // get_modifier_state: evt.get_modifier_state(), + }) + } + "select" => Arc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); + Arc::new(TouchData { + alt_key: evt.alt_key(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + shift_key: evt.shift_key(), + }) + } + "scroll" => Arc::new(()), + "wheel" => { + let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); + Arc::new(WheelData { + delta_x: evt.delta_x(), + delta_y: evt.delta_y(), + delta_z: evt.delta_z(), + delta_mode: evt.delta_mode(), + }) + } + "animationstart" | "animationend" | "animationiteration" => { + let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); + Arc::new(AnimationData { + elapsed_time: evt.elapsed_time(), + animation_name: evt.animation_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "transitionend" => { + let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); + Arc::new(TransitionData { + elapsed_time: evt.elapsed_time(), + property_name: evt.property_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), + "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), + } +} + +/// This function decodes a websys event and produces an EventTrigger +/// With the websys implementation, we attach a unique key to the nodes +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { + use anyhow::Context; + + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let element_id = target + .get_attribute("dioxus-id") + .context("Could not find element id on event target")? + .parse()?; + + Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(element_id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +fn event_name_from_typ(typ: &str) -> &'static str { + match typ { + "copy" => "copy", + "cut" => "cut", + "paste" => "paste", + "compositionend" => "compositionend", + "compositionstart" => "compositionstart", + "compositionupdate" => "compositionupdate", + "keydown" => "keydown", + "keypress" => "keypress", + "keyup" => "keyup", + "focus" => "focus", + "blur" => "blur", + "change" => "change", + "input" => "input", + "invalid" => "invalid", + "reset" => "reset", + "submit" => "submit", + "click" => "click", + "contextmenu" => "contextmenu", + "doubleclick" => "doubleclick", + "drag" => "drag", + "dragend" => "dragend", + "dragenter" => "dragenter", + "dragexit" => "dragexit", + "dragleave" => "dragleave", + "dragover" => "dragover", + "dragstart" => "dragstart", + "drop" => "drop", + "mousedown" => "mousedown", + "mouseenter" => "mouseenter", + "mouseleave" => "mouseleave", + "mousemove" => "mousemove", + "mouseout" => "mouseout", + "mouseover" => "mouseover", + "mouseup" => "mouseup", + "pointerdown" => "pointerdown", + "pointermove" => "pointermove", + "pointerup" => "pointerup", + "pointercancel" => "pointercancel", + "gotpointercapture" => "gotpointercapture", + "lostpointercapture" => "lostpointercapture", + "pointerenter" => "pointerenter", + "pointerleave" => "pointerleave", + "pointerover" => "pointerover", + "pointerout" => "pointerout", + "select" => "select", + "touchcancel" => "touchcancel", + "touchend" => "touchend", + "touchmove" => "touchmove", + "touchstart" => "touchstart", + "scroll" => "scroll", + "wheel" => "wheel", + "animationstart" => "animationstart", + "animationend" => "animationend", + "animationiteration" => "animationiteration", + "transitionend" => "transitionend", + "abort" => "abort", + "canplay" => "canplay", + "canplaythrough" => "canplaythrough", + "durationchange" => "durationchange", + "emptied" => "emptied", + "encrypted" => "encrypted", + "ended" => "ended", + "error" => "error", + "loadeddata" => "loadeddata", + "loadedmetadata" => "loadedmetadata", + "loadstart" => "loadstart", + "pause" => "pause", + "play" => "play", + "playing" => "playing", + "progress" => "progress", + "ratechange" => "ratechange", + "seeked" => "seeked", + "seeking" => "seeking", + "stalled" => "stalled", + "suspend" => "suspend", + "timeupdate" => "timeupdate", + "volumechange" => "volumechange", + "waiting" => "waiting", + "toggle" => "toggle", + _ => { + panic!("unsupported event type") + } + } +} + + +//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. + +use std::ops::{Index, IndexMut}; +use web_sys::Node; + +pub(crate) struct NodeSlab { + nodes: Vec>, +} + +impl NodeSlab { + pub fn new(capacity: usize) -> NodeSlab { + let nodes = Vec::with_capacity(capacity); + NodeSlab { nodes } + } +} +impl Index for NodeSlab { + type Output = Option; + fn index(&self, index: usize) -> &Self::Output { + &self.nodes[index] + } +} + +impl IndexMut for NodeSlab { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + if index >= self.nodes.capacity() * 3 { + panic!("Trying to mutate an element way too far out of bounds"); + } + + if index + 1 > self.nodes.len() { + self.nodes.resize_with(index + 1, || None); + } + &mut self.nodes[index] + } +} + + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 91abb4fa1..387baab59 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -80,7 +80,8 @@ impl WebsysDom { *last_node_was_text = true; - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } @@ -105,7 +106,8 @@ impl WebsysDom { .set_attribute("dioxus-id", s.as_str()) .unwrap(); - self.nodes[node_id.0] = Some(node.clone()); + self.interpreter.set_node(node_id.0, node.clone()); + // self.nodes[node_id.0] = Some(node.clone()); *cur_place += 1; @@ -120,9 +122,10 @@ impl WebsysDom { } for listener in vel.listeners { - self.new_event_listener( + self.interpreter.NewEventListener( listener.event, listener.mounted_node.get().unwrap().as_u64(), + self.handler.as_ref().unchecked_ref(), ); } @@ -142,7 +145,9 @@ impl WebsysDom { let cur_place = place.last_mut().unwrap(); let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 29c97bcba..2ac0668b9 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -68,8 +68,7 @@ impl RafLoop { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); let deadline = self.ric_receiver.recv().await.unwrap(); - let deadline = TimeoutFuture::new(deadline); - deadline + TimeoutFuture::new(deadline) } pub async fn wait_for_raf(&self) {