From 5352ef69a210cde334b834d2a43ffefb550ce9ab Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 18 May 2026 02:07:48 +0200 Subject: [PATCH] browser DOM support --- additional/dom-env.js | 558 +++++++++++++++++ examples/HelloDom/main.cpp | 153 +++++ examples/HelloDom/project.cpp | 41 ++ examples/HelloDom/serve.sh | 2 + .../Crafter.Graphics-Clipboard.cpp | 16 +- implementations/Crafter.Graphics-Dom.cpp | 571 ++++++++++++++++++ implementations/Crafter.Graphics-Gamepad.cpp | 155 +++++ implementations/Crafter.Graphics-Router.cpp | 74 +++ implementations/Crafter.Graphics-Window.cpp | 286 ++++++++- .../Crafter.Graphics-ComputeShader.cppm | 4 + interfaces/Crafter.Graphics-Decompress.cppm | 4 + ...Crafter.Graphics-DescriptorHeapVulkan.cppm | 6 +- interfaces/Crafter.Graphics-Device.cppm | 11 + interfaces/Crafter.Graphics-Dom.cppm | 217 +++++++ interfaces/Crafter.Graphics-DomEvents.cppm | 119 ++++ interfaces/Crafter.Graphics-Font.cppm | 6 +- interfaces/Crafter.Graphics-FontAtlas.cppm | 4 + interfaces/Crafter.Graphics-ImageVulkan.cppm | 6 +- interfaces/Crafter.Graphics-InputField.cppm | 5 + interfaces/Crafter.Graphics-Keys.cppm | 147 +++++ interfaces/Crafter.Graphics-Mesh.cppm | 6 +- .../Crafter.Graphics-PipelineRTVulkan.cppm | 6 +- interfaces/Crafter.Graphics-RTPass.cppm | 4 + interfaces/Crafter.Graphics-RenderPass.cppm | 4 + .../Crafter.Graphics-RenderingElement3D.cppm | 6 +- interfaces/Crafter.Graphics-Router.cppm | 50 ++ .../Crafter.Graphics-SamplerVulkan.cppm | 6 +- ...ter.Graphics-ShaderBindingTableVulkan.cppm | 6 +- interfaces/Crafter.Graphics-ShaderVulkan.cppm | 6 +- interfaces/Crafter.Graphics-Types.cppm | 6 + interfaces/Crafter.Graphics-UI.cppm | 4 + interfaces/Crafter.Graphics-UIComponents.cppm | 4 + interfaces/Crafter.Graphics-VulkanBuffer.cppm | 6 +- .../Crafter.Graphics-VulkanTransition.cppm | 6 +- interfaces/Crafter.Graphics-Window.cppm | 23 + interfaces/Crafter.Graphics.cppm | 21 +- project.cpp | 147 +++-- 37 files changed, 2637 insertions(+), 59 deletions(-) create mode 100644 additional/dom-env.js create mode 100644 examples/HelloDom/main.cpp create mode 100644 examples/HelloDom/project.cpp create mode 100755 examples/HelloDom/serve.sh create mode 100644 implementations/Crafter.Graphics-Dom.cpp create mode 100644 implementations/Crafter.Graphics-Router.cpp create mode 100644 interfaces/Crafter.Graphics-Dom.cppm create mode 100644 interfaces/Crafter.Graphics-DomEvents.cppm create mode 100644 interfaces/Crafter.Graphics-Router.cppm diff --git a/additional/dom-env.js b/additional/dom-env.js new file mode 100644 index 0000000..8e47650 --- /dev/null +++ b/additional/dom-env.js @@ -0,0 +1,558 @@ +/* +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, +}); diff --git a/examples/HelloDom/main.cpp b/examples/HelloDom/main.cpp new file mode 100644 index 0000000..503e373 --- /dev/null +++ b/examples/HelloDom/main.cpp @@ -0,0 +1,153 @@ +/* +HelloDom — exercises every public surface of the DOM partition that +absorbed Crafter.CppDOM: + + * `Window` as a page-level event hub (no rendering — that's V2 / WebGPU). + * `HtmlElement::CreateInBody` for element creation (new in this lib; + CppDOM was query-only). + * `HtmlElementPtr::Add*Listener` with auto-cleanup on destruction. + * `Router::PushState` / `AddPopStateListener` / `GetPath` driving a + real SPA — each route swaps the main content area without a server + round-trip. + +Serve the bin dir over HTTP, open index.html, navigate between Home / +About via the in-page links, hit back/forward, watch mouse/keyboard +events log into the box at the bottom. +*/ + +import Crafter.Graphics; +import Crafter.Event; +import std; +using namespace Crafter; + +// ─── SPA scaffolding ────────────────────────────────────────────────── +// The full app shape: +// +// [ nav: Home | About ] +// ──────────────────── +// [ main — replaced per route ] +// ──────────────────── +// [ log — append-only event trace ] +// +// `static` storage on the C++ side keeps the HtmlElements alive for the +// page's whole lifetime (their destructors would otherwise yank the +// nodes out of the DOM and unregister listeners when main() returns). +// Window itself is also `static` because the JS bridge stashes a raw +// pointer to it on construction; stack-local would leave that pointer +// dangling as soon as main returned. + +static std::string log; +static void Append(std::string_view line) { + log += std::string(line) + "\n"; + Dom::HtmlElementPtr pre("hello-log"); + pre.SetInnerHTML(log); +} + +// Elements scoped to the currently-rendered route. `HtmlElementPtr` +// auto-removes its registered listeners when destroyed (the V1 fix +// for CppDOM's silent-leak class of bug), so any element we want a +// click handler on must outlive `Render()` — otherwise the destructor +// kicks in immediately and the listener dies before the user can click. +// Clearing this list at the top of every Render() detaches the previous +// route's bindings; pushing into it within a branch keeps the new +// route's bindings alive until the next navigation. +static std::vector routeBindings; + +static void Render(std::string_view path) { + routeBindings.clear(); + Dom::HtmlElementPtr main("hello-main"); + if (path == "/" || path.empty()) { + main.SetInnerHTML(R"( +

Home

+

This is the home route. Click About above to + navigate without reloading.

+ + )"); + Dom::HtmlElementPtr& btn = routeBindings.emplace_back("hello-btn"); + btn.AddClickListener([](Dom::MouseEvent e) { + Append(std::format("button click @ ({:.0f},{:.0f})", e.clientX, e.clientY)); + }); + } else if (path == "/about") { + main.SetInnerHTML(R"( +

About

+

HelloDom is the demo for Crafter.Graphics's + CRAFTER_GRAPHICS_WINDOW_DOM mode. The whole + page — content, routing, event handling — is compiled + C++ running in WebAssembly.

+ )"); + } else { + main.SetInnerHTML(std::format(R"( +

Not found

+

No route registered for {}.

+ )", path)); + } + Append(std::format("rendered route: {}", path)); +} + +// Used by both the nav click handlers and (indirectly) the popstate +// listener. Mutates browser history, then re-renders. +static void Navigate(std::string_view url) { + Router::PushState("{}", "", url); + Render(url); +} + +int main() { + Device::Initialize(); + static Window window(0, 0, "HelloDom"); + + // Build the persistent page chrome. The route links use plain + // (no ) so the browser does NOT + // navigate when they're clicked — only our handler runs. This is + // the canonical SPA pattern; if you want right-click-open-in-new-tab + // you'd switch to and call preventDefault in a custom JS + // intercept (not exposed in V1). + static Dom::HtmlElement root = Dom::HtmlElement::CreateInBody("div", "hello-root"); + root.SetStyle("font-family:system-ui,sans-serif;padding:24px;max-width:640px;"); + root.SetInnerHTML(R"( + +
+

Event log

+

+    )");
+
+    // Nav link click handlers. `static` so they outlive main(); the
+    // s themselves are persistent (we never SetInnerHTML on
+    //