/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // JS bridge for the CRAFTER_GRAPHICS_WINDOW_DOM build of Crafter.Graphics. // Populates `window.crafter_webbuild_env` (same global as Crafter.CppDOM // used) with the env imports the .wasm declares. Crafter.Build's // runtime.js merges this object into the WebAssembly import object as // the `env` module before instantiation. // // Three groups of imports: // 1. DOM ops + 23 element-scoped listener kinds (port of CppDOM env.js, // plus createElement / getBody for the new element-creation API). // 2. Window-level event hookup — DOM events delivered to a global // Window object on the C++ side. Set up when the C++ Window ctor // calls domAttachWindow(). // 3. requestAnimationFrame driver — domStartFrameLoop / domStopFrameLoop. const __decoder = new TextDecoder(); const __encoder = new TextEncoder(); // jsmemory: integer cookie → live DOM element. Counter is monotone; // 32-bit wrap is unlikely over a single page's lifetime but if it // ever matters we'd switch to a free-list. let __cookieCounter = 0; const __jsmemory = new Map(); const __listenerHandlers = new Map(); // `${ptr}-${id}-${kind}` → JS handler // Window-level (document / window) listeners installed by domAttachWindow. // Indexed by event kind; each entry is the JS handler we'd remove if the // user ever destroyed the Window. V1 only ever has one Window so we don't // support tear-down; the listeners live for the page's lifetime. const __windowListeners = {}; let __windowAttachedHandle = 0; // cookie of the C++ Window for export dispatch let __frameLoopActive = false; function __wasm() { return window.crafter_webbuild_wasi.instance.exports; } function __memBuf() { return window.crafter_webbuild_wasi.instance.exports.memory.buffer; } function __readUtf8(ptr, len) { return __decoder.decode(new Uint8Array(__memBuf(), ptr, len)); } function __writeUtf8(str) { const encoded = __encoder.encode(str + '\0'); const ptr = __wasm().WasmAlloc(encoded.length); new Uint8Array(__memBuf(), ptr, encoded.length).set(encoded); return ptr; } function __storeElement(el) { __jsmemory.set(++__cookieCounter, el); return __cookieCounter; } // ─── DOM lookup / creation ──────────────────────────────────────────── function getElementById(idPtr, idLen) { try { const id = __readUtf8(idPtr, idLen); const el = document.getElementById(id); if (!el) { console.error(`Crafter.Dom: getElementById("${id}") → null`); return 0; } return __storeElement(el); } catch (err) { console.error(err); return 0; } } function createElement(parentCookie, tagPtr, tagLen, idPtr, idLen) { try { const tag = __readUtf8(tagPtr, tagLen); const el = document.createElement(tag); if (idLen > 0) { el.id = __readUtf8(idPtr, idLen); } const parent = __jsmemory.get(parentCookie); if (parent) { parent.appendChild(el); } else { // parent cookie 0 / not-found → append to body so the element // still ends up in the DOM (matches CreateInBody semantics // when the user passes a moved-from / default-constructed // HtmlElementPtr). document.body.appendChild(el); } return __storeElement(el); } catch (err) { console.error(err); return 0; } } function getBody() { return __storeElement(document.body); } function freeJs(cookie) { __jsmemory.delete(cookie); } // ─── DOM mutations ──────────────────────────────────────────────────── function setInnerHTML(cookie, htmlPtr, htmlLen) { const el = __jsmemory.get(cookie); if (el) el.innerHTML = __readUtf8(htmlPtr, htmlLen); } function setStyle(cookie, stylePtr, styleLen) { const el = __jsmemory.get(cookie); if (el) el.style.cssText = __readUtf8(stylePtr, styleLen); } function setProperty(cookie, propPtr, propLen, valPtr, valLen) { const el = __jsmemory.get(cookie); if (el) el.style.setProperty(__readUtf8(propPtr, propLen), __readUtf8(valPtr, valLen)); } function addClass(cookie, namePtr, nameLen) { const el = __jsmemory.get(cookie); if (el) el.classList.add(__readUtf8(namePtr, nameLen)); } function removeClass(cookie, namePtr, nameLen) { const el = __jsmemory.get(cookie); if (el) el.classList.remove(__readUtf8(namePtr, nameLen)); } function toggleClass(cookie, namePtr, nameLen) { const el = __jsmemory.get(cookie); if (el) el.classList.toggle(__readUtf8(namePtr, nameLen)); } function hasClass(cookie, namePtr, nameLen) { const el = __jsmemory.get(cookie); if (!el) return false; return el.classList.contains(__readUtf8(namePtr, nameLen)); } function deleteElement(cookie) { const el = __jsmemory.get(cookie); if (el && el.parentNode) el.parentNode.removeChild(el); } function getValue(cookie) { const el = __jsmemory.get(cookie); if (!el || el.value === undefined) return 0; return __writeUtf8(el.value || ""); } function setValue(cookie, valPtr, valLen) { const el = __jsmemory.get(cookie); if (el) el.value = __readUtf8(valPtr, valLen); } // ─── Element-scoped event listeners ─────────────────────────────────── // // Each Add*Listener factory generates an addEventListener call whose // handler marshals the event fields through to a specific C++ export. // We key the handler in __listenerHandlers by `${cookie}-${id}-${kind}` // so removeEventListener can re-find it. C++-side handler id counters // are per-kind, so a per-kind suffix is what makes the keys unique. function __makeMouseListenerPair(kind, eventName, exportName) { return { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { __wasm()[exportName](id, event.clientX, event.clientY, event.screenX, event.screenY, event.button, event.buttons, event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); }; __listenerHandlers.set(`${cookie}-${id}-${kind}`, handler); el.addEventListener(eventName, handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-${kind}`; const handler = __listenerHandlers.get(key); if (el && handler) el.removeEventListener(eventName, handler); __listenerHandlers.delete(key); } }; } function __makeKeyListenerPair(kind, eventName, exportName) { return { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { const keyPtr = __writeUtf8(event.key || ""); __wasm()[exportName](id, keyPtr, event.keyCode, event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); __wasm().WasmFree(keyPtr); }; __listenerHandlers.set(`${cookie}-${id}-${kind}`, handler); el.addEventListener(eventName, handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-${kind}`; const handler = __listenerHandlers.get(key); if (el && handler) el.removeEventListener(eventName, handler); __listenerHandlers.delete(key); } }; } const __mouse_Click = __makeMouseListenerPair("click", "click", "ExecuteClickHandler"); const __mouse_MouseOver = __makeMouseListenerPair("mouseover", "mouseover", "ExecuteMouseOverHandler"); const __mouse_MouseOut = __makeMouseListenerPair("mouseout", "mouseout", "ExecuteMouseOutHandler"); const __mouse_MouseMove = __makeMouseListenerPair("mousemove", "mousemove", "ExecuteMouseMoveHandler"); const __mouse_MouseDown = __makeMouseListenerPair("mousedown", "mousedown", "ExecuteMouseDownHandler"); const __mouse_MouseUp = __makeMouseListenerPair("mouseup", "mouseup", "ExecuteMouseUpHandler"); const __mouse_ContextMenu = __makeMouseListenerPair("contextmenu", "contextmenu", "ExecuteContextMenuHandler"); const __mouse_DragStart = __makeMouseListenerPair("dragstart", "dragstart", "ExecuteDragStartHandler"); const __mouse_DragEnd = __makeMouseListenerPair("dragend", "dragend", "ExecuteDragEndHandler"); const __mouse_Drop = __makeMouseListenerPair("drop", "drop", "ExecuteDropHandler"); const __mouse_DragOver = __makeMouseListenerPair("dragover", "dragover", "ExecuteDragOverHandler"); const __mouse_DragEnter = __makeMouseListenerPair("dragenter", "dragenter", "ExecuteDragEnterHandler"); const __mouse_DragLeave = __makeMouseListenerPair("dragleave", "dragleave", "ExecuteDragLeaveHandler"); const __key_KeyDown = __makeKeyListenerPair("keydown", "keydown", "ExecuteKeyDownHandler"); const __key_KeyUp = __makeKeyListenerPair("keyup", "keyup", "ExecuteKeyUpHandler"); const __key_KeyPress = __makeKeyListenerPair("keypress", "keypress", "ExecuteKeyPressHandler"); function __makeFocusListenerPair(kind, eventName, exportName) { return { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { const tCookie = event.target ? __storeElement(event.target) : 0; const rCookie = event.relatedTarget ? __storeElement(event.relatedTarget) : 0; __wasm()[exportName](id, tCookie, rCookie); }; __listenerHandlers.set(`${cookie}-${id}-${kind}`, handler); el.addEventListener(eventName, handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-${kind}`; const handler = __listenerHandlers.get(key); if (el && handler) el.removeEventListener(eventName, handler); __listenerHandlers.delete(key); } }; } const __focusPair = __makeFocusListenerPair("focus", "focus", "ExecuteFocusHandler"); const __blurPair = __makeFocusListenerPair("blur", "blur", "ExecuteBlurHandler"); // Change / Input / Submit / Resize / Scroll have bespoke marshaling. const __changePair = { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { const p = __writeUtf8(event.target.value || ""); __wasm().ExecuteChangeHandler(id, p); __wasm().WasmFree(p); }; __listenerHandlers.set(`${cookie}-${id}-change`, handler); el.addEventListener("change", handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-change`; const h = __listenerHandlers.get(key); if (el && h) el.removeEventListener("change", h); __listenerHandlers.delete(key); } }; const __inputPair = { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { const text = event.data || (event.target && event.target.value) || ""; const p = __writeUtf8(text); __wasm().ExecuteInputHandler(id, p, event.inputType === 'insertCompositionText'); __wasm().WasmFree(p); }; __listenerHandlers.set(`${cookie}-${id}-input`, handler); el.addEventListener("input", handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-input`; const h = __listenerHandlers.get(key); if (el && h) el.removeEventListener("input", h); __listenerHandlers.delete(key); } }; const __submitPair = { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { event.preventDefault(); __wasm().ExecuteSubmitHandler(id); }; __listenerHandlers.set(`${cookie}-${id}-submit`, handler); el.addEventListener("submit", handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-submit`; const h = __listenerHandlers.get(key); if (el && h) el.removeEventListener("submit", h); __listenerHandlers.delete(key); } }; const __resizePair = { // Resize is window-global in CppDOM. Mirror that: attach to `window` // regardless of which element the C++ caller passed. add(cookie, id) { const handler = () => __wasm().ExecuteResizeHandler(id, window.innerWidth, window.innerHeight); __listenerHandlers.set(`${cookie}-${id}-resize`, handler); window.addEventListener("resize", handler); }, remove(cookie, id) { const key = `${cookie}-${id}-resize`; const h = __listenerHandlers.get(key); if (h) window.removeEventListener("resize", h); __listenerHandlers.delete(key); } }; const __scrollPair = { add(cookie, id) { const handler = () => __wasm().ExecuteScrollHandler(id, window.scrollX, window.scrollY); __listenerHandlers.set(`${cookie}-${id}-scroll`, handler); window.addEventListener("scroll", handler); }, remove(cookie, id) { const key = `${cookie}-${id}-scroll`; const h = __listenerHandlers.get(key); if (h) window.removeEventListener("scroll", h); __listenerHandlers.delete(key); } }; const __wheelPair = { add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { __wasm().ExecuteWheelHandler(id, event.deltaX, event.deltaY, event.deltaZ, event.deltaMode, event.clientX, event.clientY, event.screenX, event.screenY, event.button, event.buttons, event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); }; __listenerHandlers.set(`${cookie}-${id}-wheel`, handler); el.addEventListener("wheel", handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); const key = `${cookie}-${id}-wheel`; const h = __listenerHandlers.get(key); if (el && h) el.removeEventListener("wheel", h); __listenerHandlers.delete(key); } }; // ─── Window-level event hookup ──────────────────────────────────────── // // Called by the C++ Window ctor (`domAttachWindow`). Installs document / // window listeners that route into Window exports (`__crafterDom_*`). // Only one Window per page in V1 — multiple Attach calls overwrite the // previous attachment. function domAttachWindow(windowHandle) { __windowAttachedHandle = windowHandle; const fire = (name, args) => { const fn = __wasm()[name]; if (fn) fn(__windowAttachedHandle, ...args); }; __windowListeners.mousemove = (e) => fire("__crafterDom_mouseMove", [e.clientX, e.clientY]); __windowListeners.mousedown = (e) => fire("__crafterDom_mouseDown", [e.button]); __windowListeners.mouseup = (e) => fire("__crafterDom_mouseUp", [e.button]); __windowListeners.wheel = (e) => fire("__crafterDom_wheel", [e.deltaY]); __windowListeners.contextmenu = (e) => { e.preventDefault(); }; // Keyboard events go through the document so they fire even when no // input element is focused. event.code is the layout-independent // physical key identifier — matches what the C++ Keys table hashes. __windowListeners.keydown = (e) => { const codePtr = __writeUtf8(e.code || ""); const keyPtr = __writeUtf8(e.key || ""); fire("__crafterDom_keyDown", [codePtr, e.code.length, keyPtr, e.key.length, e.repeat]); __wasm().WasmFree(codePtr); __wasm().WasmFree(keyPtr); }; __windowListeners.keyup = (e) => { const codePtr = __writeUtf8(e.code || ""); fire("__crafterDom_keyUp", [codePtr, e.code.length]); __wasm().WasmFree(codePtr); }; __windowListeners.resize = () => fire("__crafterDom_resize", [window.innerWidth, window.innerHeight]); __windowListeners.beforeunload = () => fire("__crafterDom_close", []); document.addEventListener("mousemove", __windowListeners.mousemove); document.addEventListener("mousedown", __windowListeners.mousedown); document.addEventListener("mouseup", __windowListeners.mouseup); document.addEventListener("wheel", __windowListeners.wheel); document.addEventListener("contextmenu", __windowListeners.contextmenu); document.addEventListener("keydown", __windowListeners.keydown); document.addEventListener("keyup", __windowListeners.keyup); window .addEventListener("resize", __windowListeners.resize); window .addEventListener("beforeunload",__windowListeners.beforeunload); } function domSetTitle(titlePtr, titleLen) { document.title = __readUtf8(titlePtr, titleLen); } function domGetInnerWidth() { return window.innerWidth; } function domGetInnerHeight() { return window.innerHeight; } // ─── requestAnimationFrame loop ─────────────────────────────────────── function __frameTick() { if (!__frameLoopActive) return; const fn = __wasm().__crafterDom_frame; if (fn) fn(__windowAttachedHandle); window.requestAnimationFrame(__frameTick); } function domStartFrameLoop() { if (__frameLoopActive) return; __frameLoopActive = true; window.requestAnimationFrame(__frameTick); } function domStopFrameLoop() { __frameLoopActive = false; } // ─── Clipboard ──────────────────────────────────────────────────────── function clipboardSetText(strPtr, strLen) { try { navigator.clipboard.writeText(__readUtf8(strPtr, strLen)); return true; } catch (err) { console.error("Crafter.Clipboard.SetText failed:", err); return false; } } // ─── History / routing ──────────────────────────────────────────────── function pushState(dataPtr, dataLen, titlePtr, titleLen, urlPtr, urlLen) { const dataStr = __readUtf8(dataPtr, dataLen); const titleStr = __readUtf8(titlePtr, titleLen); const urlStr = __readUtf8(urlPtr, urlLen); let parsed; try { parsed = JSON.parse(dataStr); } catch { parsed = null; } window.history.pushState(parsed, titleStr, urlStr); } function addPopStateListener(id) { const handler = () => __wasm().ExecutePopStateHandler(id); __listenerHandlers.set(`popstate-${id}`, handler); window.addEventListener("popstate", handler); } function removePopStateListener(id) { const h = __listenerHandlers.get(`popstate-${id}`); if (h) window.removeEventListener("popstate", h); __listenerHandlers.delete(`popstate-${id}`); } function getPathName() { return __writeUtf8(window.location.pathname); } // ─── Gamepad polling helper ─────────────────────────────────────────── // // V1: a minimal polling shim used by the C++ Gamepad backend. Returns // the navigator.getGamepads() result reshaped into a flat layout the // C++ side can read out via getter calls. Hot-plug events are exposed // as a flag the C++ side polls during Tick. let __gamepadConnectedFlag = false; let __gamepadDisconnectedFlag = false; window.addEventListener("gamepadconnected", () => { __gamepadConnectedFlag = true; }); window.addEventListener("gamepaddisconnected", () => { __gamepadDisconnectedFlag = true; }); function gamepadPollConnected() { const v = __gamepadConnectedFlag; __gamepadConnectedFlag = false; return v; } function gamepadPollDisconnected() { const v = __gamepadDisconnectedFlag; __gamepadDisconnectedFlag = false; return v; } function gamepadCount() { return (navigator.getGamepads ? navigator.getGamepads() : []).filter(g => g).length; } function gamepadGetButton(idx, buttonIdx) { const pads = navigator.getGamepads ? navigator.getGamepads() : []; const p = pads.filter(g => g)[idx]; return p && p.buttons[buttonIdx] ? (p.buttons[buttonIdx].pressed ? 1 : 0) : 0; } function gamepadGetAxis(idx, axisIdx) { const pads = navigator.getGamepads ? navigator.getGamepads() : []; const p = pads.filter(g => g)[idx]; return p && typeof p.axes[axisIdx] === "number" ? p.axes[axisIdx] : 0; } // ─── Export env object ──────────────────────────────────────────────── if (!window.crafter_webbuild_env) { window.crafter_webbuild_env = {}; } Object.assign(window.crafter_webbuild_env, { // DOM lookup / creation / mutation freeJs, getElementById, createElement, getBody, setInnerHTML, setStyle, setProperty, addClass, removeClass, toggleClass, hasClass, deleteElement, getValue, setValue, // Element listeners — pair add/remove addClickListener: __mouse_Click.add, removeClickListener: __mouse_Click.remove, addMouseOverListener: __mouse_MouseOver.add, removeMouseOverListener: __mouse_MouseOver.remove, addMouseOutListener: __mouse_MouseOut.add, removeMouseOutListener: __mouse_MouseOut.remove, addMouseMoveListener: __mouse_MouseMove.add, removeMouseMoveListener: __mouse_MouseMove.remove, addMouseDownListener: __mouse_MouseDown.add, removeMouseDownListener: __mouse_MouseDown.remove, addMouseUpListener: __mouse_MouseUp.add, removeMouseUpListener: __mouse_MouseUp.remove, addContextMenuListener: __mouse_ContextMenu.add, removeContextMenuListener: __mouse_ContextMenu.remove, addDragStartListener: __mouse_DragStart.add, removeDragStartListener: __mouse_DragStart.remove, addDragEndListener: __mouse_DragEnd.add, removeDragEndListener: __mouse_DragEnd.remove, addDropListener: __mouse_Drop.add, removeDropListener: __mouse_Drop.remove, addDragOverListener: __mouse_DragOver.add, removeDragOverListener: __mouse_DragOver.remove, addDragEnterListener: __mouse_DragEnter.add, removeDragEnterListener: __mouse_DragEnter.remove, addDragLeaveListener: __mouse_DragLeave.add, removeDragLeaveListener: __mouse_DragLeave.remove, addKeyDownListener: __key_KeyDown.add, removeKeyDownListener: __key_KeyDown.remove, addKeyUpListener: __key_KeyUp.add, removeKeyUpListener: __key_KeyUp.remove, addKeyPressListener: __key_KeyPress.add, removeKeyPressListener: __key_KeyPress.remove, addFocusListener: __focusPair.add, removeFocusListener: __focusPair.remove, addBlurListener: __blurPair.add, removeBlurListener: __blurPair.remove, addChangeListener: __changePair.add, removeChangeListener: __changePair.remove, addInputListener: __inputPair.add, removeInputListener: __inputPair.remove, addSubmitListener: __submitPair.add, removeSubmitListener: __submitPair.remove, addResizeListener: __resizePair.add, removeResizeListener: __resizePair.remove, addScrollListener: __scrollPair.add, removeScrollListener: __scrollPair.remove, addWheelListener: __wheelPair.add, removeWheelListener: __wheelPair.remove, // Window / lifecycle domAttachWindow, domSetTitle, domGetInnerWidth, domGetInnerHeight, domStartFrameLoop, domStopFrameLoop, // Clipboard clipboardSetText, // History pushState, addPopStateListener, removePopStateListener, getPathName, // Gamepad gamepadPollConnected, gamepadPollDisconnected, gamepadCount, gamepadGetButton, gamepadGetAxis, });