2026-05-18 02:07:48 +02:00
|
|
|
/*
|
|
|
|
|
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.
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
// devicePixelRatio scaling factor. dom-webgpu.js sets window.crafter_dpr
|
|
|
|
|
// during its canvas sync so this side and the GPU side agree on a single
|
|
|
|
|
// physical-pixel coordinate space. Fallback to the live DPR if no GPU
|
|
|
|
|
// bridge ran (pure-CppDOM apps); ultimately fallback to 1 so non-HiDPI
|
|
|
|
|
// browsers behave as before.
|
|
|
|
|
function __dpr() {
|
|
|
|
|
return window.crafter_dpr || window.devicePixelRatio || 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
function __makeMouseListenerPair(kind, eventName, exportName) {
|
|
|
|
|
return {
|
|
|
|
|
add(cookie, id) {
|
|
|
|
|
const el = __jsmemory.get(cookie);
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const handler = (event) => {
|
2026-05-24 13:32:08 +02:00
|
|
|
const s = __dpr();
|
2026-05-18 02:07:48 +02:00
|
|
|
__wasm()[exportName](id,
|
2026-05-24 13:32:08 +02:00
|
|
|
event.clientX * s, event.clientY * s,
|
|
|
|
|
event.screenX * s, event.screenY * s,
|
2026-05-18 02:07:48 +02:00
|
|
|
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) {
|
2026-05-24 13:32:08 +02:00
|
|
|
const handler = () => {
|
|
|
|
|
const s = __dpr();
|
|
|
|
|
__wasm().ExecuteResizeHandler(id, window.innerWidth * s, window.innerHeight * s);
|
|
|
|
|
};
|
2026-05-18 02:07:48 +02:00
|
|
|
__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) => {
|
2026-05-24 13:32:08 +02:00
|
|
|
const s = __dpr();
|
2026-05-18 02:07:48 +02:00
|
|
|
__wasm().ExecuteWheelHandler(id,
|
|
|
|
|
event.deltaX, event.deltaY, event.deltaZ, event.deltaMode,
|
2026-05-24 13:32:08 +02:00
|
|
|
event.clientX * s, event.clientY * s, event.screenX * s, event.screenY * s,
|
2026-05-18 02:07:48 +02:00
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
// Synthetic absolute position for pointer-lock mode. While the
|
|
|
|
|
// pointer is locked, browsers fire mousemove events with movementX/Y
|
|
|
|
|
// deltas instead of meaningful clientX/Y, and the cursor is hidden +
|
|
|
|
|
// captured by the canvas (no window-edge clamp). We accumulate the
|
|
|
|
|
// deltas into a synthetic position and feed *that* to the C++ side,
|
|
|
|
|
// so the existing `currentMousePos - lastMousePos` delta computation
|
|
|
|
|
// keeps working unchanged. Initialised to the cursor position the
|
|
|
|
|
// moment lock is acquired.
|
|
|
|
|
let __ptrLockSyntheticX = 0;
|
|
|
|
|
let __ptrLockSyntheticY = 0;
|
|
|
|
|
const __isPointerLocked = () =>
|
|
|
|
|
document.pointerLockElement !== null &&
|
|
|
|
|
document.pointerLockElement !== undefined;
|
|
|
|
|
|
|
|
|
|
// pointermove (not mousemove) so we can pull sub-frame events out of
|
|
|
|
|
// `getCoalescedEvents()`. Browsers normally collapse multiple raw
|
|
|
|
|
// mouse events between paint frames into a single event you'd see
|
|
|
|
|
// via `mousemove`; PointerEvent.getCoalescedEvents() returns the raw
|
|
|
|
|
// pre-coalesced list. Summing those gives a higher-resolution delta
|
|
|
|
|
// per frame than the single coalesced movementX/Y. PointerEvent also
|
|
|
|
|
// delivers fractional movementX from high-precision mice on Chromium.
|
|
|
|
|
__windowListeners.mousemove = (e) => {
|
|
|
|
|
const s = __dpr();
|
|
|
|
|
const locked = __isPointerLocked();
|
|
|
|
|
if (locked) {
|
|
|
|
|
// Accumulate over every sub-frame event the browser had
|
|
|
|
|
// queued up. `getCoalescedEvents` is the spec-correct way
|
|
|
|
|
// to access raw input between rAF ticks. Some browsers
|
|
|
|
|
// return an empty list — fall back to the top-level event.
|
|
|
|
|
let dx = 0, dy = 0;
|
|
|
|
|
const sub = (typeof e.getCoalescedEvents === "function")
|
|
|
|
|
? e.getCoalescedEvents() : null;
|
|
|
|
|
if (sub && sub.length > 0) {
|
|
|
|
|
for (let i = 0; i < sub.length; i++) {
|
|
|
|
|
dx += sub[i].movementX;
|
|
|
|
|
dy += sub[i].movementY;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
dx = e.movementX;
|
|
|
|
|
dy = e.movementY;
|
|
|
|
|
}
|
|
|
|
|
// No DPR scaling in pointer-lock: position is synthetic and
|
|
|
|
|
// there's no UI hit-test using it. DPR-scaling here only
|
|
|
|
|
// rounds finer movements up to multiples of `dpr`, which is
|
|
|
|
|
// pure quantization loss for aim controls.
|
|
|
|
|
__ptrLockSyntheticX += dx;
|
|
|
|
|
__ptrLockSyntheticY += dy;
|
|
|
|
|
fire("__crafterDom_mouseMove",
|
|
|
|
|
[__ptrLockSyntheticX, __ptrLockSyntheticY]);
|
|
|
|
|
} else {
|
|
|
|
|
fire("__crafterDom_mouseMove", [e.clientX * s, e.clientY * s]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
__windowListeners.mousedown = (e) => {
|
|
|
|
|
// Right-click holds engage pointer lock — typical FPS-camera
|
|
|
|
|
// convention. Acquiring on any click (the previous policy) made
|
|
|
|
|
// menus annoying: clicking a button hid the cursor mid-flow. Now
|
|
|
|
|
// the cursor stays free for clicks/menus until the user holds
|
|
|
|
|
// RMB to actively look around. Browsers require lock requests
|
|
|
|
|
// from user gestures, which mousedown satisfies.
|
|
|
|
|
if (e.button === 2 && !__isPointerLocked()) {
|
|
|
|
|
const target = document.body;
|
|
|
|
|
if (target && target.requestPointerLock) {
|
|
|
|
|
target.requestPointerLock();
|
|
|
|
|
// Seed the synthetic position from the click point so
|
|
|
|
|
// there's no jump when the lock starts producing deltas.
|
|
|
|
|
__ptrLockSyntheticX = e.clientX;
|
|
|
|
|
__ptrLockSyntheticY = e.clientY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fire("__crafterDom_mouseDown", [e.button]);
|
|
|
|
|
};
|
|
|
|
|
__windowListeners.mouseup = (e) => {
|
|
|
|
|
// Release lock on RMB up — cursor reappears at the seed point
|
|
|
|
|
// for clicks/menus until the next RMB hold.
|
|
|
|
|
if (e.button === 2 && __isPointerLocked()) {
|
|
|
|
|
document.exitPointerLock();
|
|
|
|
|
}
|
|
|
|
|
fire("__crafterDom_mouseUp", [e.button]);
|
|
|
|
|
};
|
2026-05-18 02:07:48 +02:00
|
|
|
__windowListeners.wheel = (e) => fire("__crafterDom_wheel", [e.deltaY]);
|
|
|
|
|
__windowListeners.contextmenu = (e) => { e.preventDefault(); };
|
2026-05-24 13:32:08 +02:00
|
|
|
__windowListeners.pointerlockchange = () => {
|
|
|
|
|
// Reset the synthetic accumulator when lock is released so the
|
|
|
|
|
// next acquisition starts cleanly. The C++ side will see one
|
|
|
|
|
// small jump back to the real cursor position on release.
|
|
|
|
|
if (!__isPointerLocked()) {
|
|
|
|
|
__ptrLockSyntheticX = 0;
|
|
|
|
|
__ptrLockSyntheticY = 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-18 02:07:48 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
__windowListeners.resize = () => {
|
|
|
|
|
const s = __dpr();
|
|
|
|
|
fire("__crafterDom_resize", [window.innerWidth * s, window.innerHeight * s]);
|
|
|
|
|
};
|
2026-05-18 02:07:48 +02:00
|
|
|
__windowListeners.beforeunload = () => fire("__crafterDom_close", []);
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
// pointermove (not mousemove) so the handler receives PointerEvents
|
|
|
|
|
// and can use getCoalescedEvents() to recover sub-frame motion. The
|
|
|
|
|
// handler's variable name stays "mousemove" — it's the same JS object,
|
|
|
|
|
// just bound to a different event type.
|
|
|
|
|
document.addEventListener("pointermove", __windowListeners.mousemove);
|
2026-05-18 02:07:48 +02:00
|
|
|
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);
|
2026-05-24 13:32:08 +02:00
|
|
|
document.addEventListener("pointerlockchange", __windowListeners.pointerlockchange);
|
2026-05-18 02:07:48 +02:00
|
|
|
window .addEventListener("resize", __windowListeners.resize);
|
|
|
|
|
window .addEventListener("beforeunload",__windowListeners.beforeunload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function domSetTitle(titlePtr, titleLen) {
|
|
|
|
|
document.title = __readUtf8(titlePtr, titleLen);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
function domGetInnerWidth() { return Math.round(window.innerWidth * __dpr()); }
|
|
|
|
|
function domGetInnerHeight() { return Math.round(window.innerHeight * __dpr()); }
|
2026-05-18 02:07:48 +02:00
|
|
|
|
|
|
|
|
// ─── 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 ────────────────────────────────────────────────────────
|
2026-05-19 00:45:22 +02:00
|
|
|
//
|
|
|
|
|
// Read path is necessarily a latched buffer: navigator.clipboard.readText
|
|
|
|
|
// is async (Promise) but the C++ Clipboard::GetText signature is
|
|
|
|
|
// synchronous to match the Wayland / Win32 backends. Two feeders keep
|
|
|
|
|
// the cache fresh:
|
|
|
|
|
//
|
|
|
|
|
// 1. A capture-phase `paste` listener on `window` reads
|
|
|
|
|
// event.clipboardData.getData('text/plain') synchronously — this
|
|
|
|
|
// covers the standard Ctrl+V flow, where the paste event fires
|
|
|
|
|
// in the same task as the C++ key handler that will call GetText.
|
|
|
|
|
// 2. Each GetText call also kicks off a navigator.clipboard.readText
|
|
|
|
|
// in the background. The promise resolves in a later task; the
|
|
|
|
|
// first call usually still returns the latched paste value (or
|
|
|
|
|
// nullopt), subsequent calls see the readText result.
|
|
|
|
|
|
|
|
|
|
let __clipboardCache = null;
|
|
|
|
|
|
|
|
|
|
window.addEventListener("paste", (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const t = event.clipboardData &&
|
|
|
|
|
event.clipboardData.getData("text/plain");
|
|
|
|
|
if (typeof t === "string") __clipboardCache = t;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Some browsers reject clipboardData access inside non-input
|
|
|
|
|
// targets; the async readText fallback still has a chance.
|
|
|
|
|
}
|
|
|
|
|
}, true);
|
2026-05-18 02:07:48 +02:00
|
|
|
|
|
|
|
|
function clipboardSetText(strPtr, strLen) {
|
|
|
|
|
try {
|
|
|
|
|
navigator.clipboard.writeText(__readUtf8(strPtr, strLen));
|
|
|
|
|
return true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Crafter.Clipboard.SetText failed:", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 00:45:22 +02:00
|
|
|
function clipboardGetText() {
|
|
|
|
|
// Best-effort async refresh. Permission/focus rejections are
|
|
|
|
|
// expected in non-activated contexts — swallow them so we don't
|
|
|
|
|
// spam the console on every paste check.
|
|
|
|
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
|
|
|
navigator.clipboard.readText()
|
|
|
|
|
.then((t) => { if (typeof t === "string") __clipboardCache = t; })
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
if (__clipboardCache === null) return 0;
|
|
|
|
|
return __writeUtf8(__clipboardCache);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
// ─── 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
|
2026-05-19 00:45:22 +02:00
|
|
|
clipboardSetText, clipboardGetText,
|
2026-05-18 02:07:48 +02:00
|
|
|
|
|
|
|
|
// History
|
|
|
|
|
pushState, addPopStateListener, removePopStateListener, getPathName,
|
|
|
|
|
|
|
|
|
|
// Gamepad
|
|
|
|
|
gamepadPollConnected, gamepadPollDisconnected,
|
|
|
|
|
gamepadCount, gamepadGetButton, gamepadGetAxis,
|
|
|
|
|
});
|