browser DOM support
This commit is contained in:
parent
3859c43ce3
commit
5352ef69a2
37 changed files with 2637 additions and 59 deletions
558
additional/dom-env.js
Normal file
558
additional/dom-env.js
Normal file
|
|
@ -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,
|
||||||
|
});
|
||||||
153
examples/HelloDom/main.cpp
Normal file
153
examples/HelloDom/main.cpp
Normal file
|
|
@ -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<Dom::HtmlElementPtr> routeBindings;
|
||||||
|
|
||||||
|
static void Render(std::string_view path) {
|
||||||
|
routeBindings.clear();
|
||||||
|
Dom::HtmlElementPtr main("hello-main");
|
||||||
|
if (path == "/" || path.empty()) {
|
||||||
|
main.SetInnerHTML(R"(
|
||||||
|
<h2>Home</h2>
|
||||||
|
<p>This is the home route. Click <b>About</b> above to
|
||||||
|
navigate without reloading.</p>
|
||||||
|
<button id="hello-btn">Click me</button>
|
||||||
|
)");
|
||||||
|
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"(
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>HelloDom is the demo for Crafter.Graphics's
|
||||||
|
<code>CRAFTER_GRAPHICS_WINDOW_DOM</code> mode. The whole
|
||||||
|
page — content, routing, event handling — is compiled
|
||||||
|
C++ running in WebAssembly.</p>
|
||||||
|
)");
|
||||||
|
} else {
|
||||||
|
main.SetInnerHTML(std::format(R"(
|
||||||
|
<h2>Not found</h2>
|
||||||
|
<p>No route registered for <code>{}</code>.</p>
|
||||||
|
)", 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
|
||||||
|
// <span class="route-link"> (no <a href>) 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 <a href> 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"(
|
||||||
|
<nav style="margin-bottom:16px">
|
||||||
|
<span id="link-home" class="route-link" style="cursor:pointer;text-decoration:underline;margin-right:12px">Home</span>
|
||||||
|
<span id="link-about" class="route-link" style="cursor:pointer;text-decoration:underline">About</span>
|
||||||
|
</nav>
|
||||||
|
<div id="hello-main" style="min-height:120px;padding:12px;border:1px solid #ddd;border-radius:6px"></div>
|
||||||
|
<h3 style="margin-top:24px;font-size:14px;text-transform:uppercase;color:#666">Event log</h3>
|
||||||
|
<pre id="hello-log" style="background:#eee;padding:8px;height:160px;overflow:auto;font-size:12px"></pre>
|
||||||
|
)");
|
||||||
|
|
||||||
|
// Nav link click handlers. `static` so they outlive main(); the
|
||||||
|
// <span>s themselves are persistent (we never SetInnerHTML on
|
||||||
|
// <nav>), so the same listener fires for every nav click.
|
||||||
|
static Dom::HtmlElementPtr linkHome("link-home");
|
||||||
|
static Dom::HtmlElementPtr linkAbout("link-about");
|
||||||
|
linkHome .AddClickListener([](Dom::MouseEvent) { Navigate("/"); });
|
||||||
|
linkAbout.AddClickListener([](Dom::MouseEvent) { Navigate("/about"); });
|
||||||
|
|
||||||
|
// Browser back / forward: re-render whatever the URL now points to.
|
||||||
|
Router::AddPopStateListener([] {
|
||||||
|
std::string path = Router::GetPath();
|
||||||
|
Append(std::format("popstate → {}", path));
|
||||||
|
Render(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial render — boot into whatever path the page loaded with.
|
||||||
|
Append("page initialised");
|
||||||
|
Render(Router::GetPath());
|
||||||
|
|
||||||
|
// Page-level events come through Window. KeyDown fires for every
|
||||||
|
// key press regardless of focus; matches Wayland / Win32 semantics.
|
||||||
|
static EventListener<KeyCode> keyDownSub(&window.onRawKeyDown,
|
||||||
|
[](KeyCode code) {
|
||||||
|
Append(std::format("key down: 0x{:x}", code));
|
||||||
|
});
|
||||||
|
static EventListener<std::uint32_t> scrollSub(&window.onMouseScroll,
|
||||||
|
[](std::uint32_t delta) {
|
||||||
|
Append(std::format("scroll: {}", static_cast<std::int32_t>(delta)));
|
||||||
|
});
|
||||||
|
static EventListener<void> resizeSub(&window.onResize,
|
||||||
|
[]{ Append("resize"); });
|
||||||
|
|
||||||
|
// Kick off the rAF loop. Returns immediately on DOM; the wasm
|
||||||
|
// module stays alive while the JS bridge ticks frames.
|
||||||
|
window.StartUpdate();
|
||||||
|
window.StartSync();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
41
examples/HelloDom/project.cpp
Normal file
41
examples/HelloDom/project.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
HelloDom — the minimum DOM-mode example. Build with:
|
||||||
|
crafter-build --local --target=wasm32-wasip1
|
||||||
|
|
||||||
|
Output lands in bin/HelloDom-wasm32-wasip1-native-native-<hash>/. The
|
||||||
|
runtime expects a co-located dom-env.js (provided by Crafter.Graphics
|
||||||
|
itself) plus a runtime.js / index.html pair that wires the env into the
|
||||||
|
WebAssembly imports — see EnableWasiBrowserRuntime in Crafter.Build.
|
||||||
|
*/
|
||||||
|
import std;
|
||||||
|
import Crafter.Build;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||||
|
depArgs.push_back("--target=wasm32-wasip1");
|
||||||
|
Configuration* graphics = LocalProject({
|
||||||
|
.projectFile = "../../project.cpp",
|
||||||
|
.args = depArgs,
|
||||||
|
});
|
||||||
|
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.path = "./";
|
||||||
|
cfg.name = "HelloDom";
|
||||||
|
cfg.outputName = "HelloDom";
|
||||||
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
cfg.target = "wasm32-wasip1";
|
||||||
|
ApplyStandardArgs(cfg, args);
|
||||||
|
cfg.dependencies = { graphics };
|
||||||
|
|
||||||
|
std::array<fs::path, 0> ifaces = {};
|
||||||
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
|
// Generates index.html + runtime.js in the bin dir so the .wasm can
|
||||||
|
// be loaded directly in a browser via a static file server.
|
||||||
|
EnableWasiBrowserRuntime(cfg);
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
2
examples/HelloDom/serve.sh
Executable file
2
examples/HelloDom/serve.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
caddy file-server --listen :8080 --root bin/HelloDom-wasm32-wasip1-native-native-df37fe0fe124fe57
|
||||||
|
|
@ -98,8 +98,22 @@ namespace {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// Single JS import — the env.js implementation calls navigator.clipboard.writeText.
|
||||||
|
// `bool` return mirrors the native paths so callers don't need to #ifdef the
|
||||||
|
// check; failure cases (no permission, async rejection) just log to console.
|
||||||
|
// Lives in a named namespace (anonymous would give internal linkage and the
|
||||||
|
// import_module attribute would be dropped at link time).
|
||||||
|
namespace Crafter::DomEnv {
|
||||||
|
__attribute__((import_module("env"), import_name("clipboardSetText")))
|
||||||
|
bool DomClipboardSetText(const char* text, std::int32_t len);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool Crafter::Clipboard::SetText(std::string_view text) {
|
bool Crafter::Clipboard::SetText(std::string_view text) {
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
return Crafter::DomEnv::DomClipboardSetText(text.data(), static_cast<std::int32_t>(text.size()));
|
||||||
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
|
||||||
if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
|
if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
|
||||||
// Compositor doesn't expose wl_data_device_manager (rare; some
|
// Compositor doesn't expose wl_data_device_manager (rare; some
|
||||||
// headless / minimal compositors). Caller can fall back.
|
// headless / minimal compositors). Caller can fall back.
|
||||||
|
|
|
||||||
571
implementations/Crafter.Graphics-Dom.cpp
Normal file
571
implementations/Crafter.Graphics-Dom.cpp
Normal file
|
|
@ -0,0 +1,571 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Implementation of the :Dom partition. Only ever built when
|
||||||
|
// CRAFTER_GRAPHICS_WINDOW_DOM is defined (project.cpp's DOM branch
|
||||||
|
// is what brings this file into the source list). All WASM imports
|
||||||
|
// resolve against the `env` module produced by additional/dom-env.js;
|
||||||
|
// all exports become C entry points the JS bridge calls back through.
|
||||||
|
|
||||||
|
module;
|
||||||
|
module Crafter.Graphics;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter::Dom;
|
||||||
|
|
||||||
|
// ─── WASM env imports + per-event-kind handler maps ───────────────────
|
||||||
|
//
|
||||||
|
// External linkage required for the import_module/import_name attributes
|
||||||
|
// to actually wire up to a WASM env symbol — anonymous namespace would
|
||||||
|
// hide them. The namespace also gates access from the extern "C"
|
||||||
|
// dispatcher block below.
|
||||||
|
namespace Crafter::DomBindings {
|
||||||
|
// DOM ops (string args are pointer/length pairs — the JS side decodes
|
||||||
|
// UTF-8 directly out of WASM linear memory; no copying on the C++
|
||||||
|
// side).
|
||||||
|
__attribute__((import_module("env"), import_name("freeJs")))
|
||||||
|
void FreeJs(std::int32_t ptr);
|
||||||
|
__attribute__((import_module("env"), import_name("getElementById")))
|
||||||
|
std::int32_t GetElementById(const char* id, std::int32_t idLength);
|
||||||
|
__attribute__((import_module("env"), import_name("createElement")))
|
||||||
|
std::int32_t CreateElement(std::int32_t parentPtr,
|
||||||
|
const char* tagName, std::int32_t tagNameLength,
|
||||||
|
const char* id, std::int32_t idLength);
|
||||||
|
__attribute__((import_module("env"), import_name("getBody")))
|
||||||
|
std::int32_t GetBody();
|
||||||
|
__attribute__((import_module("env"), import_name("setInnerHTML")))
|
||||||
|
void SetInnerHTML(std::int32_t ptr, const char* html, std::int32_t htmlLength);
|
||||||
|
__attribute__((import_module("env"), import_name("setStyle")))
|
||||||
|
void SetStyle(std::int32_t ptr, const char* style, std::int32_t styleLength);
|
||||||
|
__attribute__((import_module("env"), import_name("setProperty")))
|
||||||
|
void SetProperty(std::int32_t ptr,
|
||||||
|
const char* property, std::int32_t propertyLength,
|
||||||
|
const char* value, std::int32_t valueLength);
|
||||||
|
__attribute__((import_module("env"), import_name("addClass")))
|
||||||
|
void AddClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("removeClass")))
|
||||||
|
void RemoveClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("toggleClass")))
|
||||||
|
void ToggleClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("hasClass")))
|
||||||
|
bool HasClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("deleteElement")))
|
||||||
|
void DeleteElement(std::int32_t ptr);
|
||||||
|
__attribute__((import_module("env"), import_name("getValue")))
|
||||||
|
const char* GetValue(std::int32_t ptr);
|
||||||
|
__attribute__((import_module("env"), import_name("setValue")))
|
||||||
|
void SetValue(std::int32_t ptr, const char* value, std::int32_t valueLength);
|
||||||
|
|
||||||
|
// Per-event-kind listener register / unregister imports.
|
||||||
|
#define CG_DOM_LISTENER_IMPORT(addName, removeName) \
|
||||||
|
__attribute__((import_module("env"), import_name(#addName))) \
|
||||||
|
void addName(std::int32_t ptr, std::int32_t id); \
|
||||||
|
__attribute__((import_module("env"), import_name(#removeName))) \
|
||||||
|
void removeName(std::int32_t ptr, std::int32_t id);
|
||||||
|
|
||||||
|
CG_DOM_LISTENER_IMPORT(addClickListener, removeClickListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addMouseOverListener, removeMouseOverListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addMouseOutListener, removeMouseOutListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addMouseMoveListener, removeMouseMoveListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addMouseDownListener, removeMouseDownListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addMouseUpListener, removeMouseUpListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addFocusListener, removeFocusListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addBlurListener, removeBlurListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addKeyDownListener, removeKeyDownListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addKeyUpListener, removeKeyUpListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addKeyPressListener, removeKeyPressListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addChangeListener, removeChangeListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addSubmitListener, removeSubmitListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addInputListener, removeInputListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addResizeListener, removeResizeListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addScrollListener, removeScrollListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addContextMenuListener, removeContextMenuListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDragStartListener, removeDragStartListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDragEndListener, removeDragEndListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDropListener, removeDropListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDragOverListener, removeDragOverListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDragEnterListener, removeDragEnterListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addDragLeaveListener, removeDragLeaveListener)
|
||||||
|
CG_DOM_LISTENER_IMPORT(addWheelListener, removeWheelListener)
|
||||||
|
#undef CG_DOM_LISTENER_IMPORT
|
||||||
|
|
||||||
|
// Per-event-kind callback maps. Counters are per-kind so two
|
||||||
|
// different event kinds can share an id without aliasing — the JS
|
||||||
|
// bridge keys its own handler table by (ptr, id, kind) for the same
|
||||||
|
// reason. `maxId` is bumped without wrap-around handling: 2 billion
|
||||||
|
// listeners over the lifetime of a single page is enough.
|
||||||
|
template <typename EvT>
|
||||||
|
struct HandlerTable {
|
||||||
|
std::int32_t maxId = 0;
|
||||||
|
std::unordered_map<std::int32_t, std::function<void(EvT)>> map;
|
||||||
|
};
|
||||||
|
template <>
|
||||||
|
struct HandlerTable<void> {
|
||||||
|
std::int32_t maxId = 0;
|
||||||
|
std::unordered_map<std::int32_t, std::function<void()>> map;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline HandlerTable<MouseEvent> clickT, mouseOverT, mouseOutT, mouseMoveT,
|
||||||
|
mouseDownT, mouseUpT, contextMenuT,
|
||||||
|
dragStartT, dragEndT, dropT,
|
||||||
|
dragOverT, dragEnterT, dragLeaveT;
|
||||||
|
inline HandlerTable<WheelEvent> wheelT;
|
||||||
|
inline HandlerTable<FocusEvent> focusT, blurT;
|
||||||
|
inline HandlerTable<KeyboardEvent> keyDownT, keyUpT, keyPressT;
|
||||||
|
inline HandlerTable<ChangeEvent> changeT;
|
||||||
|
inline HandlerTable<InputEvent> inputT;
|
||||||
|
inline HandlerTable<ResizeEvent> resizeT;
|
||||||
|
inline HandlerTable<ScrollEvent> scrollT;
|
||||||
|
inline HandlerTable<void> submitT;
|
||||||
|
inline HandlerTable<void> popStateT;
|
||||||
|
inline HandlerTable<std::string> fetchT; // Reserved for future use.
|
||||||
|
|
||||||
|
// Opaque cross-TU helpers for the popState table. Router.cpp lives
|
||||||
|
// in a different translation unit and can't touch HandlerTable
|
||||||
|
// (re-defining the specialization there would be an ODR violation);
|
||||||
|
// it goes through these instead. Defined below the namespace.
|
||||||
|
std::int32_t PopStateRegister(std::function<void()> cb);
|
||||||
|
void PopStateUnregister(std::int32_t id);
|
||||||
|
|
||||||
|
// The 24 stable indices used by HtmlElementPtr::handlerIds_[] to
|
||||||
|
// map the per-kind id list back to the right `remove*Listener`
|
||||||
|
// import on destruction. Order MUST match the cppm comment.
|
||||||
|
enum class Kind : std::uint8_t {
|
||||||
|
Click = 0, MouseOver, MouseOut, MouseMove, MouseDown, MouseUp,
|
||||||
|
Focus, Blur, KeyDown, KeyUp, KeyPress, Change, Submit, Input,
|
||||||
|
Resize, Scroll, ContextMenu, DragStart, DragEnd, Drop,
|
||||||
|
DragOver, DragEnter, DragLeave, Wheel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WASM exports the JS bridge calls back through ────────────────────
|
||||||
|
//
|
||||||
|
// One per event kind plus the popstate / fetch dispatchers. Each takes
|
||||||
|
// the handler id + the marshaled event fields, looks up the table, and
|
||||||
|
// invokes the std::function. find()->second is intentional: a callback
|
||||||
|
// firing for an id we never registered is a bridge bug worth crashing
|
||||||
|
// on (the env.js side stores its own handler table keyed by id and
|
||||||
|
// only calls Execute*Handler for ids it minted).
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
__attribute__((export_name("WasmAlloc")))
|
||||||
|
void* WasmAlloc(std::int32_t size) { return std::malloc(size); }
|
||||||
|
|
||||||
|
__attribute__((export_name("WasmFree")))
|
||||||
|
void WasmFree(void* ptr) { std::free(ptr); }
|
||||||
|
|
||||||
|
#define CG_DOM_EXEC_MOUSE(exportName, table) \
|
||||||
|
__attribute__((export_name(#exportName))) \
|
||||||
|
void exportName(std::int32_t handlerID, \
|
||||||
|
double clientX, double clientY, \
|
||||||
|
double screenX, double screenY, \
|
||||||
|
std::int32_t button, std::int32_t buttons, \
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) { \
|
||||||
|
Crafter::DomBindings::table.map.find(handlerID)->second( \
|
||||||
|
MouseEvent(clientX, clientY, screenX, screenY, \
|
||||||
|
button, buttons, altKey, ctrlKey, shiftKey, metaKey)); \
|
||||||
|
}
|
||||||
|
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteClickHandler, clickT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteMouseOverHandler, mouseOverT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteMouseOutHandler, mouseOutT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteMouseMoveHandler, mouseMoveT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteMouseDownHandler, mouseDownT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteMouseUpHandler, mouseUpT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteContextMenuHandler, contextMenuT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDragStartHandler, dragStartT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDragEndHandler, dragEndT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDropHandler, dropT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDragOverHandler, dragOverT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDragEnterHandler, dragEnterT)
|
||||||
|
CG_DOM_EXEC_MOUSE(ExecuteDragLeaveHandler, dragLeaveT)
|
||||||
|
#undef CG_DOM_EXEC_MOUSE
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteWheelHandler")))
|
||||||
|
void ExecuteWheelHandler(std::int32_t handlerID,
|
||||||
|
double deltaX, double deltaY, double deltaZ, std::int32_t /*deltaMode*/,
|
||||||
|
double clientX, double clientY, double screenX, double screenY,
|
||||||
|
std::int32_t button, std::int32_t buttons,
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) {
|
||||||
|
Crafter::DomBindings::wheelT.map.find(handlerID)->second(
|
||||||
|
WheelEvent(clientX, clientY, screenX, screenY,
|
||||||
|
button, buttons, altKey, ctrlKey, shiftKey, metaKey,
|
||||||
|
deltaX, deltaY, deltaZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteFocusHandler")))
|
||||||
|
void ExecuteFocusHandler(std::int32_t handlerID, void* target, void* relatedTarget) {
|
||||||
|
Crafter::DomBindings::focusT.map.find(handlerID)->second(
|
||||||
|
FocusEvent(target, relatedTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteBlurHandler")))
|
||||||
|
void ExecuteBlurHandler(std::int32_t handlerID, void* target, void* relatedTarget) {
|
||||||
|
Crafter::DomBindings::blurT.map.find(handlerID)->second(
|
||||||
|
FocusEvent(target, relatedTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CG_DOM_EXEC_KEY(exportName, table) \
|
||||||
|
__attribute__((export_name(#exportName))) \
|
||||||
|
void exportName(std::int32_t handlerID, \
|
||||||
|
const char* key, std::int32_t keyCode, \
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey) { \
|
||||||
|
Crafter::DomBindings::table.map.find(handlerID)->second( \
|
||||||
|
KeyboardEvent(key, keyCode, altKey, ctrlKey, shiftKey, metaKey)); \
|
||||||
|
}
|
||||||
|
|
||||||
|
CG_DOM_EXEC_KEY(ExecuteKeyDownHandler, keyDownT)
|
||||||
|
CG_DOM_EXEC_KEY(ExecuteKeyUpHandler, keyUpT)
|
||||||
|
CG_DOM_EXEC_KEY(ExecuteKeyPressHandler, keyPressT)
|
||||||
|
#undef CG_DOM_EXEC_KEY
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteChangeHandler")))
|
||||||
|
void ExecuteChangeHandler(std::int32_t handlerID, const char* value) {
|
||||||
|
Crafter::DomBindings::changeT.map.find(handlerID)->second(ChangeEvent(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteSubmitHandler")))
|
||||||
|
void ExecuteSubmitHandler(std::int32_t handlerID) {
|
||||||
|
Crafter::DomBindings::submitT.map.find(handlerID)->second();
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteInputHandler")))
|
||||||
|
void ExecuteInputHandler(std::int32_t handlerID, const char* data, bool isComposing) {
|
||||||
|
Crafter::DomBindings::inputT.map.find(handlerID)->second(InputEvent(data, isComposing));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteResizeHandler")))
|
||||||
|
void ExecuteResizeHandler(std::int32_t handlerID, std::int32_t width, std::int32_t height) {
|
||||||
|
Crafter::DomBindings::resizeT.map.find(handlerID)->second(ResizeEvent(width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecuteScrollHandler")))
|
||||||
|
void ExecuteScrollHandler(std::int32_t handlerID, double scrollX, double scrollY) {
|
||||||
|
Crafter::DomBindings::scrollT.map.find(handlerID)->second(ScrollEvent(scrollX, scrollY));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("ExecutePopStateHandler")))
|
||||||
|
void ExecutePopStateHandler(std::int32_t handlerID) {
|
||||||
|
Crafter::DomBindings::popStateT.map.find(handlerID)->second();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Listener add/remove helpers ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Each Add*Listener follows the same five-step pattern; capturing it in
|
||||||
|
// a single function template removes ~120 lines of near-duplicate body
|
||||||
|
// code. AddImpl returns the freshly minted id (already pushed onto the
|
||||||
|
// per-element tracking list). RemoveImpl performs the inverse for an
|
||||||
|
// id known to belong to the right table.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
template <typename EvT, typename JsAdd>
|
||||||
|
std::int32_t AddImpl(Crafter::DomBindings::HandlerTable<EvT>& table,
|
||||||
|
std::int32_t ptr,
|
||||||
|
std::vector<std::int32_t>& trackingList,
|
||||||
|
JsAdd jsAdd,
|
||||||
|
std::function<void(EvT)> callback) {
|
||||||
|
if (ptr == 0) return 0;
|
||||||
|
std::int32_t id = table.maxId++;
|
||||||
|
table.map.insert({id, std::move(callback)});
|
||||||
|
jsAdd(ptr, id);
|
||||||
|
trackingList.push_back(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
template <typename JsAdd>
|
||||||
|
std::int32_t AddVoidImpl(Crafter::DomBindings::HandlerTable<void>& table,
|
||||||
|
std::int32_t ptr,
|
||||||
|
std::vector<std::int32_t>& trackingList,
|
||||||
|
JsAdd jsAdd,
|
||||||
|
std::function<void()> callback) {
|
||||||
|
if (ptr == 0) return 0;
|
||||||
|
std::int32_t id = table.maxId++;
|
||||||
|
table.map.insert({id, std::move(callback)});
|
||||||
|
jsAdd(ptr, id);
|
||||||
|
trackingList.push_back(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Table, typename JsRemove>
|
||||||
|
void RemoveImpl(Table& table,
|
||||||
|
std::int32_t ptr,
|
||||||
|
std::vector<std::int32_t>& trackingList,
|
||||||
|
JsRemove jsRemove,
|
||||||
|
std::int32_t id) {
|
||||||
|
if (ptr == 0) return;
|
||||||
|
table.map.erase(id);
|
||||||
|
trackingList.erase(std::remove(trackingList.begin(), trackingList.end(), id),
|
||||||
|
trackingList.end());
|
||||||
|
jsRemove(ptr, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HtmlElementPtr ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace Crafter::Dom {
|
||||||
|
|
||||||
|
HtmlElementPtr::HtmlElementPtr(const std::string_view id)
|
||||||
|
: ptr(Crafter::DomBindings::GetElementById(id.data(), id.size())) {}
|
||||||
|
|
||||||
|
HtmlElementPtr::HtmlElementPtr(const std::string_view id, const std::string_view html)
|
||||||
|
: ptr(Crafter::DomBindings::GetElementById(id.data(), id.size())) {
|
||||||
|
if (ptr != 0) {
|
||||||
|
Crafter::DomBindings::SetInnerHTML(ptr, html.data(), html.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElementPtr::HtmlElementPtr(HtmlElementPtr&& other) noexcept
|
||||||
|
: ptr(other.ptr) {
|
||||||
|
other.ptr = 0;
|
||||||
|
for (std::size_t i = 0; i < 24; ++i) handlerIds_[i] = std::move(other.handlerIds_[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElementPtr& HtmlElementPtr::operator=(HtmlElementPtr&& other) noexcept {
|
||||||
|
if (this == &other) return *this;
|
||||||
|
RemoveAllHandlersAndFree();
|
||||||
|
ptr = other.ptr;
|
||||||
|
other.ptr = 0;
|
||||||
|
for (std::size_t i = 0; i < 24; ++i) handlerIds_[i] = std::move(other.handlerIds_[i]);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElementPtr::~HtmlElementPtr() {
|
||||||
|
RemoveAllHandlersAndFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HtmlElementPtr::RemoveAllHandlersAndFree() {
|
||||||
|
if (ptr == 0) return;
|
||||||
|
// Unregister every still-live handler the user forgot about.
|
||||||
|
// The per-kind index matches the cppm comment ordering and the
|
||||||
|
// Kind enum above; keep them in sync.
|
||||||
|
namespace D = Crafter::DomBindings;
|
||||||
|
auto sweep = [this](auto& table, auto jsRemove, std::size_t kind) {
|
||||||
|
for (std::int32_t id : handlerIds_[kind]) {
|
||||||
|
table.map.erase(id);
|
||||||
|
jsRemove(ptr, id);
|
||||||
|
}
|
||||||
|
handlerIds_[kind].clear();
|
||||||
|
};
|
||||||
|
sweep(D::clickT, D::removeClickListener, (std::size_t)D::Kind::Click);
|
||||||
|
sweep(D::mouseOverT, D::removeMouseOverListener, (std::size_t)D::Kind::MouseOver);
|
||||||
|
sweep(D::mouseOutT, D::removeMouseOutListener, (std::size_t)D::Kind::MouseOut);
|
||||||
|
sweep(D::mouseMoveT, D::removeMouseMoveListener, (std::size_t)D::Kind::MouseMove);
|
||||||
|
sweep(D::mouseDownT, D::removeMouseDownListener, (std::size_t)D::Kind::MouseDown);
|
||||||
|
sweep(D::mouseUpT, D::removeMouseUpListener, (std::size_t)D::Kind::MouseUp);
|
||||||
|
sweep(D::focusT, D::removeFocusListener, (std::size_t)D::Kind::Focus);
|
||||||
|
sweep(D::blurT, D::removeBlurListener, (std::size_t)D::Kind::Blur);
|
||||||
|
sweep(D::keyDownT, D::removeKeyDownListener, (std::size_t)D::Kind::KeyDown);
|
||||||
|
sweep(D::keyUpT, D::removeKeyUpListener, (std::size_t)D::Kind::KeyUp);
|
||||||
|
sweep(D::keyPressT, D::removeKeyPressListener, (std::size_t)D::Kind::KeyPress);
|
||||||
|
sweep(D::changeT, D::removeChangeListener, (std::size_t)D::Kind::Change);
|
||||||
|
sweep(D::submitT, D::removeSubmitListener, (std::size_t)D::Kind::Submit);
|
||||||
|
sweep(D::inputT, D::removeInputListener, (std::size_t)D::Kind::Input);
|
||||||
|
sweep(D::resizeT, D::removeResizeListener, (std::size_t)D::Kind::Resize);
|
||||||
|
sweep(D::scrollT, D::removeScrollListener, (std::size_t)D::Kind::Scroll);
|
||||||
|
sweep(D::contextMenuT, D::removeContextMenuListener, (std::size_t)D::Kind::ContextMenu);
|
||||||
|
sweep(D::dragStartT, D::removeDragStartListener, (std::size_t)D::Kind::DragStart);
|
||||||
|
sweep(D::dragEndT, D::removeDragEndListener, (std::size_t)D::Kind::DragEnd);
|
||||||
|
sweep(D::dropT, D::removeDropListener, (std::size_t)D::Kind::Drop);
|
||||||
|
sweep(D::dragOverT, D::removeDragOverListener, (std::size_t)D::Kind::DragOver);
|
||||||
|
sweep(D::dragEnterT, D::removeDragEnterListener, (std::size_t)D::Kind::DragEnter);
|
||||||
|
sweep(D::dragLeaveT, D::removeDragLeaveListener, (std::size_t)D::Kind::DragLeave);
|
||||||
|
sweep(D::wheelT, D::removeWheelListener, (std::size_t)D::Kind::Wheel);
|
||||||
|
D::FreeJs(ptr);
|
||||||
|
ptr = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM ops are thin pass-throughs; the JS side does the work.
|
||||||
|
void HtmlElementPtr::SetInnerHTML(const std::string_view html) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetInnerHTML(ptr, html.data(), html.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::SetStyle(const std::string_view style) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetStyle(ptr, style.data(), style.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::SetProperty(const std::string_view property, const std::string_view value) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetProperty(ptr,
|
||||||
|
property.data(), property.size(),
|
||||||
|
value.data(), value.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::AddClass(const std::string_view className) {
|
||||||
|
if (ptr) Crafter::DomBindings::AddClass(ptr, className.data(), className.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::RemoveClass(const std::string_view className) {
|
||||||
|
if (ptr) Crafter::DomBindings::RemoveClass(ptr, className.data(), className.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::ToggleClass(const std::string_view className) {
|
||||||
|
if (ptr) Crafter::DomBindings::ToggleClass(ptr, className.data(), className.size());
|
||||||
|
}
|
||||||
|
bool HtmlElementPtr::HasClass(const std::string_view className) {
|
||||||
|
return ptr && Crafter::DomBindings::HasClass(ptr, className.data(), className.size());
|
||||||
|
}
|
||||||
|
std::string HtmlElementPtr::GetValue() {
|
||||||
|
if (!ptr) return {};
|
||||||
|
// The JS side WasmAlloc's a NUL-terminated buffer. We own it
|
||||||
|
// after the call — copy into std::string and free.
|
||||||
|
const char* raw = Crafter::DomBindings::GetValue(ptr);
|
||||||
|
if (!raw) return {};
|
||||||
|
std::string out(raw);
|
||||||
|
std::free(const_cast<char*>(raw));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::SetValue(const std::string_view value) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetValue(ptr, value.data(), value.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener wrappers. Each Add/Remove pair just plugs the right
|
||||||
|
// table, kind index, and js import into the helper templates. The
|
||||||
|
// 23 (+1 for popstate, lived in :Router) listener kinds previously
|
||||||
|
// expanded to ~250 lines of body; here they're ~60.
|
||||||
|
|
||||||
|
#define CG_DOM_ADD(MethodName, JsAdd, TableName, EvType, KindEnum) \
|
||||||
|
std::int32_t HtmlElementPtr::MethodName(std::function<void(EvType)> cb) { \
|
||||||
|
return AddImpl<EvType>(Crafter::DomBindings::TableName, ptr, \
|
||||||
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum], \
|
||||||
|
Crafter::DomBindings::JsAdd, std::move(cb)); \
|
||||||
|
}
|
||||||
|
#define CG_DOM_REMOVE(MethodName, JsRemove, TableName, KindEnum) \
|
||||||
|
void HtmlElementPtr::MethodName(std::int32_t id) { \
|
||||||
|
RemoveImpl(Crafter::DomBindings::TableName, ptr, \
|
||||||
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum], \
|
||||||
|
Crafter::DomBindings::JsRemove, id); \
|
||||||
|
}
|
||||||
|
|
||||||
|
CG_DOM_ADD (AddClickListener, addClickListener, clickT, MouseEvent, Click)
|
||||||
|
CG_DOM_REMOVE(RemoveClickListener, removeClickListener, clickT, Click)
|
||||||
|
CG_DOM_ADD (AddMouseOverListener, addMouseOverListener, mouseOverT, MouseEvent, MouseOver)
|
||||||
|
CG_DOM_REMOVE(RemoveMouseOverListener, removeMouseOverListener, mouseOverT, MouseOver)
|
||||||
|
CG_DOM_ADD (AddMouseOutListener, addMouseOutListener, mouseOutT, MouseEvent, MouseOut)
|
||||||
|
CG_DOM_REMOVE(RemoveMouseOutListener, removeMouseOutListener, mouseOutT, MouseOut)
|
||||||
|
CG_DOM_ADD (AddMouseMoveListener, addMouseMoveListener, mouseMoveT, MouseEvent, MouseMove)
|
||||||
|
CG_DOM_REMOVE(RemoveMouseMoveListener, removeMouseMoveListener, mouseMoveT, MouseMove)
|
||||||
|
CG_DOM_ADD (AddMouseDownListener, addMouseDownListener, mouseDownT, MouseEvent, MouseDown)
|
||||||
|
CG_DOM_REMOVE(RemoveMouseDownListener, removeMouseDownListener, mouseDownT, MouseDown)
|
||||||
|
CG_DOM_ADD (AddMouseUpListener, addMouseUpListener, mouseUpT, MouseEvent, MouseUp)
|
||||||
|
CG_DOM_REMOVE(RemoveMouseUpListener, removeMouseUpListener, mouseUpT, MouseUp)
|
||||||
|
CG_DOM_ADD (AddFocusListener, addFocusListener, focusT, FocusEvent, Focus)
|
||||||
|
CG_DOM_REMOVE(RemoveFocusListener, removeFocusListener, focusT, Focus)
|
||||||
|
CG_DOM_ADD (AddBlurListener, addBlurListener, blurT, FocusEvent, Blur)
|
||||||
|
CG_DOM_REMOVE(RemoveBlurListener, removeBlurListener, blurT, Blur)
|
||||||
|
CG_DOM_ADD (AddKeyDownListener, addKeyDownListener, keyDownT, KeyboardEvent, KeyDown)
|
||||||
|
CG_DOM_REMOVE(RemoveKeyDownListener, removeKeyDownListener, keyDownT, KeyDown)
|
||||||
|
CG_DOM_ADD (AddKeyUpListener, addKeyUpListener, keyUpT, KeyboardEvent, KeyUp)
|
||||||
|
CG_DOM_REMOVE(RemoveKeyUpListener, removeKeyUpListener, keyUpT, KeyUp)
|
||||||
|
CG_DOM_ADD (AddKeyPressListener, addKeyPressListener, keyPressT, KeyboardEvent, KeyPress)
|
||||||
|
CG_DOM_REMOVE(RemoveKeyPressListener, removeKeyPressListener, keyPressT, KeyPress)
|
||||||
|
CG_DOM_ADD (AddChangeListener, addChangeListener, changeT, ChangeEvent, Change)
|
||||||
|
CG_DOM_REMOVE(RemoveChangeListener, removeChangeListener, changeT, Change)
|
||||||
|
CG_DOM_ADD (AddInputListener, addInputListener, inputT, InputEvent, Input)
|
||||||
|
CG_DOM_REMOVE(RemoveInputListener, removeInputListener, inputT, Input)
|
||||||
|
CG_DOM_ADD (AddResizeListener, addResizeListener, resizeT, ResizeEvent, Resize)
|
||||||
|
CG_DOM_REMOVE(RemoveResizeListener, removeResizeListener, resizeT, Resize)
|
||||||
|
CG_DOM_ADD (AddScrollListener, addScrollListener, scrollT, ScrollEvent, Scroll)
|
||||||
|
CG_DOM_REMOVE(RemoveScrollListener, removeScrollListener, scrollT, Scroll)
|
||||||
|
CG_DOM_ADD (AddContextMenuListener, addContextMenuListener, contextMenuT, MouseEvent, ContextMenu)
|
||||||
|
CG_DOM_REMOVE(RemoveContextMenuListener,removeContextMenuListener,contextMenuT, ContextMenu)
|
||||||
|
CG_DOM_ADD (AddDragStartListener, addDragStartListener, dragStartT, MouseEvent, DragStart)
|
||||||
|
CG_DOM_REMOVE(RemoveDragStartListener, removeDragStartListener, dragStartT, DragStart)
|
||||||
|
CG_DOM_ADD (AddDragEndListener, addDragEndListener, dragEndT, MouseEvent, DragEnd)
|
||||||
|
CG_DOM_REMOVE(RemoveDragEndListener, removeDragEndListener, dragEndT, DragEnd)
|
||||||
|
CG_DOM_ADD (AddDropListener, addDropListener, dropT, MouseEvent, Drop)
|
||||||
|
CG_DOM_REMOVE(RemoveDropListener, removeDropListener, dropT, Drop)
|
||||||
|
CG_DOM_ADD (AddDragOverListener, addDragOverListener, dragOverT, MouseEvent, DragOver)
|
||||||
|
CG_DOM_REMOVE(RemoveDragOverListener, removeDragOverListener, dragOverT, DragOver)
|
||||||
|
CG_DOM_ADD (AddDragEnterListener, addDragEnterListener, dragEnterT, MouseEvent, DragEnter)
|
||||||
|
CG_DOM_REMOVE(RemoveDragEnterListener, removeDragEnterListener, dragEnterT, DragEnter)
|
||||||
|
CG_DOM_ADD (AddDragLeaveListener, addDragLeaveListener, dragLeaveT, MouseEvent, DragLeave)
|
||||||
|
CG_DOM_REMOVE(RemoveDragLeaveListener, removeDragLeaveListener, dragLeaveT, DragLeave)
|
||||||
|
CG_DOM_ADD (AddWheelListener, addWheelListener, wheelT, WheelEvent, Wheel)
|
||||||
|
CG_DOM_REMOVE(RemoveWheelListener, removeWheelListener, wheelT, Wheel)
|
||||||
|
#undef CG_DOM_ADD
|
||||||
|
#undef CG_DOM_REMOVE
|
||||||
|
|
||||||
|
// Submit takes std::function<void()> — needs the void specialization.
|
||||||
|
std::int32_t HtmlElementPtr::AddSubmitListener(std::function<void()> cb) {
|
||||||
|
return AddVoidImpl(Crafter::DomBindings::submitT, ptr,
|
||||||
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::Submit],
|
||||||
|
Crafter::DomBindings::addSubmitListener, std::move(cb));
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::RemoveSubmitListener(std::int32_t id) {
|
||||||
|
RemoveImpl(Crafter::DomBindings::submitT, ptr,
|
||||||
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::Submit],
|
||||||
|
Crafter::DomBindings::removeSubmitListener, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HtmlElement ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
HtmlElement::HtmlElement(const std::string_view id)
|
||||||
|
: HtmlElementPtr(id) {}
|
||||||
|
|
||||||
|
HtmlElement::HtmlElement(const std::string_view id, const std::string_view html)
|
||||||
|
: HtmlElementPtr(id, html) {}
|
||||||
|
|
||||||
|
HtmlElement::HtmlElement(HtmlElement&& other) noexcept
|
||||||
|
: HtmlElementPtr(std::move(other)) {}
|
||||||
|
|
||||||
|
HtmlElement& HtmlElement::operator=(HtmlElement&& other) noexcept {
|
||||||
|
if (this == &other) return *this;
|
||||||
|
HtmlElementPtr::operator=(std::move(other));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElement::~HtmlElement() {
|
||||||
|
// Remove the element from the DOM first. The base destructor
|
||||||
|
// runs next (implicit) and will sweep handler maps + call
|
||||||
|
// FreeJs on the still-valid JS cookie. DeleteElement only
|
||||||
|
// removes the node from its parent; the cookie itself stays
|
||||||
|
// alive in jsmemory until FreeJs.
|
||||||
|
if (ptr != 0) {
|
||||||
|
Crafter::DomBindings::DeleteElement(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElement HtmlElement::Create(const HtmlElementPtr& parent,
|
||||||
|
std::string_view tagName,
|
||||||
|
std::string_view id) {
|
||||||
|
std::int32_t handle = Crafter::DomBindings::CreateElement(
|
||||||
|
parent.ptr,
|
||||||
|
tagName.data(), static_cast<std::int32_t>(tagName.size()),
|
||||||
|
id.data(), static_cast<std::int32_t>(id.size()));
|
||||||
|
return HtmlElement(HtmlElementPtr::FromHandle{handle});
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElement HtmlElement::CreateInBody(std::string_view tagName,
|
||||||
|
std::string_view id) {
|
||||||
|
std::int32_t body = Crafter::DomBindings::GetBody();
|
||||||
|
std::int32_t handle = Crafter::DomBindings::CreateElement(
|
||||||
|
body,
|
||||||
|
tagName.data(), static_cast<std::int32_t>(tagName.size()),
|
||||||
|
id.data(), static_cast<std::int32_t>(id.size()));
|
||||||
|
Crafter::DomBindings::FreeJs(body);
|
||||||
|
return HtmlElement(HtmlElementPtr::FromHandle{handle});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sibling-TU helpers exposed for Router.cpp. Keep the popStateT table
|
||||||
|
// private to this TU and let Router go through these.
|
||||||
|
namespace Crafter::DomBindings {
|
||||||
|
std::int32_t PopStateRegister(std::function<void()> cb) {
|
||||||
|
std::int32_t id = popStateT.maxId++;
|
||||||
|
popStateT.map.insert({id, std::move(cb)});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
void PopStateUnregister(std::int32_t id) {
|
||||||
|
popStateT.map.erase(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -675,6 +675,161 @@ void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
// DOM backend (browser Gamepad API via env.js polling helpers)
|
||||||
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
namespace Crafter::DomEnv {
|
||||||
|
// External linkage required for the import_module attribute to bind
|
||||||
|
// against the env.js function. An anonymous namespace would drop the
|
||||||
|
// attribute at link time.
|
||||||
|
__attribute__((import_module("env"), import_name("gamepadPollConnected")))
|
||||||
|
bool gamepadPollConnected();
|
||||||
|
__attribute__((import_module("env"), import_name("gamepadPollDisconnected")))
|
||||||
|
bool gamepadPollDisconnected();
|
||||||
|
__attribute__((import_module("env"), import_name("gamepadCount")))
|
||||||
|
std::int32_t gamepadCount();
|
||||||
|
__attribute__((import_module("env"), import_name("gamepadGetButton")))
|
||||||
|
std::int32_t gamepadGetButton(std::int32_t idx, std::int32_t buttonIdx);
|
||||||
|
__attribute__((import_module("env"), import_name("gamepadGetAxis")))
|
||||||
|
double gamepadGetAxis(std::int32_t idx, std::int32_t axisIdx);
|
||||||
|
}
|
||||||
|
namespace {
|
||||||
|
using Crafter::DomEnv::gamepadPollConnected;
|
||||||
|
using Crafter::DomEnv::gamepadPollDisconnected;
|
||||||
|
using Crafter::DomEnv::gamepadCount;
|
||||||
|
using Crafter::DomEnv::gamepadGetButton;
|
||||||
|
using Crafter::DomEnv::gamepadGetAxis;
|
||||||
|
|
||||||
|
// Standard W3C Gamepad mapping: indices match the "standard" layout
|
||||||
|
// every browser exposes for Xbox / DualShock / DualSense controllers.
|
||||||
|
// Mismatched / non-standard pads fall through with zeroed buttons.
|
||||||
|
constexpr int kStdBtnSouth = 0;
|
||||||
|
constexpr int kStdBtnEast = 1;
|
||||||
|
constexpr int kStdBtnWest = 2;
|
||||||
|
constexpr int kStdBtnNorth = 3;
|
||||||
|
constexpr int kStdBtnLB = 4;
|
||||||
|
constexpr int kStdBtnRB = 5;
|
||||||
|
constexpr int kStdBtnLT = 6;
|
||||||
|
constexpr int kStdBtnRT = 7;
|
||||||
|
constexpr int kStdBtnSelect = 8;
|
||||||
|
constexpr int kStdBtnStart = 9;
|
||||||
|
constexpr int kStdBtnLStickClick = 10;
|
||||||
|
constexpr int kStdBtnRStickClick = 11;
|
||||||
|
constexpr int kStdBtnDPadUp = 12;
|
||||||
|
constexpr int kStdBtnDPadDown = 13;
|
||||||
|
constexpr int kStdBtnDPadLeft = 14;
|
||||||
|
constexpr int kStdBtnDPadRight = 15;
|
||||||
|
constexpr int kStdBtnHome = 16;
|
||||||
|
|
||||||
|
constexpr int kStdAxisLX = 0, kStdAxisLY = 1, kStdAxisRX = 2, kStdAxisRY = 3;
|
||||||
|
|
||||||
|
// Map our `Gamepad::Button` ordinal to the W3C standard button index.
|
||||||
|
int StdButtonIdx(Crafter::Gamepad::Button b) {
|
||||||
|
using B = Crafter::Gamepad::Button;
|
||||||
|
switch (b) {
|
||||||
|
case B::South: return kStdBtnSouth;
|
||||||
|
case B::East: return kStdBtnEast;
|
||||||
|
case B::West: return kStdBtnWest;
|
||||||
|
case B::North: return kStdBtnNorth;
|
||||||
|
case B::Select: return kStdBtnSelect;
|
||||||
|
case B::Start: return kStdBtnStart;
|
||||||
|
case B::Home: return kStdBtnHome;
|
||||||
|
case B::LeftStickClick: return kStdBtnLStickClick;
|
||||||
|
case B::RightStickClick: return kStdBtnRStickClick;
|
||||||
|
case B::LeftBumper: return kStdBtnLB;
|
||||||
|
case B::RightBumper: return kStdBtnRB;
|
||||||
|
case B::DPadUp: return kStdBtnDPadUp;
|
||||||
|
case B::DPadDown: return kStdBtnDPadDown;
|
||||||
|
case B::DPadLeft: return kStdBtnDPadLeft;
|
||||||
|
case B::DPadRight: return kStdBtnDPadRight;
|
||||||
|
case B::LeftTrigger: return kStdBtnLT;
|
||||||
|
case B::RightTrigger: return kStdBtnRT;
|
||||||
|
default: return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Crafter::Gamepad::Tick() {
|
||||||
|
// Handle connect/disconnect by rebuilding `connected` from scratch
|
||||||
|
// when the JS flags say something changed. Cheap — only fires on
|
||||||
|
// actual hot-plug, not every frame.
|
||||||
|
if (gamepadPollConnected() || gamepadPollDisconnected() || connected.empty()) {
|
||||||
|
// Snapshot previously-known ids so we can fire onDisconnected
|
||||||
|
// for ones that went away in this rebuild.
|
||||||
|
std::vector<std::uint32_t> previousIds;
|
||||||
|
previousIds.reserve(connected.size());
|
||||||
|
for (auto& d : connected) previousIds.push_back(d->id);
|
||||||
|
|
||||||
|
std::int32_t n = gamepadCount();
|
||||||
|
connected.clear();
|
||||||
|
for (std::int32_t i = 0; i < n; ++i) {
|
||||||
|
auto dev = std::make_unique<Device>();
|
||||||
|
dev->id = static_cast<std::uint32_t>(i); // index-based; stable for the page
|
||||||
|
dev->name = "Gamepad";
|
||||||
|
connected.push_back(std::move(dev));
|
||||||
|
}
|
||||||
|
// Fire connected events for ids that weren't in the previous set.
|
||||||
|
for (auto& d : connected) {
|
||||||
|
bool wasKnown = false;
|
||||||
|
for (std::uint32_t prev : previousIds) if (prev == d->id) { wasKnown = true; break; }
|
||||||
|
if (!wasKnown) onConnected.Invoke(d.get());
|
||||||
|
}
|
||||||
|
// No way to call onDisconnected with the right Device* once it's
|
||||||
|
// gone — fire with nullptr so subscribers know SOMETHING changed.
|
||||||
|
for (std::uint32_t prev : previousIds) {
|
||||||
|
bool stillThere = false;
|
||||||
|
for (auto& d : connected) if (d->id == prev) { stillThere = true; break; }
|
||||||
|
if (!stillThere) onDisconnected.Invoke(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll buttons + axes on every Tick — same shape as the native paths.
|
||||||
|
for (auto& devUp : connected) {
|
||||||
|
Device& dev = *devUp;
|
||||||
|
std::int32_t idx = static_cast<std::int32_t>(dev.id);
|
||||||
|
|
||||||
|
for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) {
|
||||||
|
int stdIdx = StdButtonIdx(static_cast<Button>(b));
|
||||||
|
bool newState = stdIdx >= 0 && gamepadGetButton(idx, stdIdx) != 0;
|
||||||
|
if (newState != dev.buttons[b]) {
|
||||||
|
dev.buttons[b] = newState;
|
||||||
|
if (newState) dev.onButtonDown.Invoke(static_cast<Button>(b));
|
||||||
|
else dev.onButtonUp .Invoke(static_cast<Button>(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Axes — sticks come straight through, triggers come from button
|
||||||
|
// analog values (the standard mapping exposes those too, with
|
||||||
|
// buttons[6/7].value, but the polling shim returns binary
|
||||||
|
// pressed-state. For V1, triggers report 0 / 1 only).
|
||||||
|
float lx = static_cast<float>(gamepadGetAxis(idx, kStdAxisLX));
|
||||||
|
float ly = static_cast<float>(gamepadGetAxis(idx, kStdAxisLY));
|
||||||
|
float rx = static_cast<float>(gamepadGetAxis(idx, kStdAxisRX));
|
||||||
|
float ry = static_cast<float>(gamepadGetAxis(idx, kStdAxisRY));
|
||||||
|
auto setAxis = [&](Axis a, float v) {
|
||||||
|
std::size_t i = (std::size_t)a;
|
||||||
|
if (dev.axes[i] != v) {
|
||||||
|
dev.axes[i] = v;
|
||||||
|
dev.onAxisChanged.Invoke(a);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setAxis(Axis::LeftStickX, lx);
|
||||||
|
setAxis(Axis::LeftStickY, ly);
|
||||||
|
setAxis(Axis::RightStickX, rx);
|
||||||
|
setAxis(Axis::RightStickY, ry);
|
||||||
|
setAxis(Axis::LeftTrigger, gamepadGetButton(idx, kStdBtnLT) ? 1.0f : 0.0f);
|
||||||
|
setAxis(Axis::RightTrigger, gamepadGetButton(idx, kStdBtnRT) ? 1.0f : 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Crafter::Gamepad::Rumble(Device& /*dev*/, float /*low*/, float /*high*/,
|
||||||
|
std::chrono::milliseconds /*duration*/) {
|
||||||
|
// Browser Gamepad rumble (HapticActuator API) is patchy across
|
||||||
|
// engines. V1: no-op. Pads that don't support it would silently
|
||||||
|
// fall through anyway.
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
|
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
|
||||||
for (auto& up : connected) {
|
for (auto& up : connected) {
|
||||||
if (up->id == id) return up.get();
|
if (up->id == id) return up.get();
|
||||||
|
|
|
||||||
74
implementations/Crafter.Graphics-Router.cpp
Normal file
74
implementations/Crafter.Graphics-Router.cpp
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Router implementation. The popState handler table lives in
|
||||||
|
// Crafter.Graphics-Dom.cpp (alongside every other event-kind table);
|
||||||
|
// we reach it through the opaque `PopStateRegister`/`PopStateUnregister`
|
||||||
|
// helpers it exposes, avoiding any cross-TU duplication of the
|
||||||
|
// HandlerTable definition.
|
||||||
|
|
||||||
|
module;
|
||||||
|
module Crafter.Graphics;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
namespace Crafter::DomBindings {
|
||||||
|
__attribute__((import_module("env"), import_name("pushState")))
|
||||||
|
void PushState(const char* data, std::int32_t dataLength,
|
||||||
|
const char* title, std::int32_t titleLength,
|
||||||
|
const char* url, std::int32_t urlLength);
|
||||||
|
__attribute__((import_module("env"), import_name("addPopStateListener")))
|
||||||
|
void AddPopStateListener(std::int32_t id);
|
||||||
|
__attribute__((import_module("env"), import_name("removePopStateListener")))
|
||||||
|
void RemovePopStateListener(std::int32_t id);
|
||||||
|
__attribute__((import_module("env"), import_name("getPathName")))
|
||||||
|
const char* GetPathName();
|
||||||
|
|
||||||
|
// Defined in Crafter.Graphics-Dom.cpp.
|
||||||
|
std::int32_t PopStateRegister(std::function<void()> cb);
|
||||||
|
void PopStateUnregister(std::int32_t id);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Crafter::Router {
|
||||||
|
|
||||||
|
void PushState(std::string_view data, std::string_view title, std::string_view url) {
|
||||||
|
Crafter::DomBindings::PushState(
|
||||||
|
data.data(), static_cast<std::int32_t>(data.size()),
|
||||||
|
title.data(), static_cast<std::int32_t>(title.size()),
|
||||||
|
url.data(), static_cast<std::int32_t>(url.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t AddPopStateListener(std::function<void()> callback) {
|
||||||
|
std::int32_t id = Crafter::DomBindings::PopStateRegister(std::move(callback));
|
||||||
|
Crafter::DomBindings::AddPopStateListener(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemovePopStateListener(std::int32_t id) {
|
||||||
|
Crafter::DomBindings::RemovePopStateListener(id);
|
||||||
|
Crafter::DomBindings::PopStateUnregister(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetPath() {
|
||||||
|
const char* raw = Crafter::DomBindings::GetPathName();
|
||||||
|
if (!raw) return {};
|
||||||
|
std::string out(raw);
|
||||||
|
std::free(const_cast<char*>(raw));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,19 +44,28 @@ module;
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
#include "vulkan/vulkan_wayland.h"
|
#include "vulkan/vulkan_wayland.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
#include "vulkan/vulkan_win32.h"
|
#include "vulkan/vulkan_win32.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
#include "../lib/stb_image_write.h"
|
#include "../lib/stb_image_write.h"
|
||||||
|
#endif
|
||||||
module Crafter.Graphics:Window_impl;
|
module Crafter.Graphics:Window_impl;
|
||||||
import :Window;
|
import :Window;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :Gamepad;
|
import :Gamepad;
|
||||||
|
// The Vulkan-typed partitions exist as empty stubs in DOM builds (the
|
||||||
|
// build system scans `import :X` statements pre-preprocessor, so even
|
||||||
|
// guarded imports must resolve to a real partition). Their bodies are
|
||||||
|
// gated under !CRAFTER_GRAPHICS_WINDOW_DOM so DOM compiles see empty
|
||||||
|
// modules. Cheap.
|
||||||
import :VulkanTransition;
|
import :VulkanTransition;
|
||||||
import :DescriptorHeapVulkan;
|
import :DescriptorHeapVulkan;
|
||||||
import :RenderPass;
|
import :RenderPass;
|
||||||
|
|
@ -64,7 +73,7 @@ import std;
|
||||||
|
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
void randname(char *buf) {
|
void randname(char *buf) {
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
|
|
@ -1247,3 +1256,276 @@ void Window::SaveFrame(const std::filesystem::path& path) {
|
||||||
vkDestroyBuffer(Device::device, stagingBuf, nullptr);
|
vkDestroyBuffer(Device::device, stagingBuf, nullptr);
|
||||||
vkFreeMemory(Device::device, stagingMem, nullptr);
|
vkFreeMemory(Device::device, stagingMem, nullptr);
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// DOM backend
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// In DOM mode the "window" IS the browser page; there is no separate
|
||||||
|
// surface to create, no swapchain to manage, no GPU pipeline to wait on.
|
||||||
|
// All Window does here is:
|
||||||
|
// - mirror requested title onto document.title
|
||||||
|
// - register itself with the JS bridge so DOM-level events route into
|
||||||
|
// its event objects
|
||||||
|
// - hand the frame loop to requestAnimationFrame; `Update` runs on
|
||||||
|
// each rAF tick when `updating` is true
|
||||||
|
//
|
||||||
|
// The C exports (__crafterDom_*) below are how the JS bridge reaches
|
||||||
|
// back into the live Window instance. We keep a process-global pointer
|
||||||
|
// for V1 — only one Window per page — and lookups are O(1).
|
||||||
|
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
|
// The JS runtime initializes itself before main() runs, so there's
|
||||||
|
// nothing to do here. Defined as a no-op so user code calling
|
||||||
|
// `Device::Initialize()` links the same way it does on native.
|
||||||
|
void Device::Initialize() {}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Window* g_domWindow = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Crafter::DomEnv {
|
||||||
|
__attribute__((import_module("env"), import_name("domAttachWindow")))
|
||||||
|
void domAttachWindow(std::int32_t handle);
|
||||||
|
__attribute__((import_module("env"), import_name("domSetTitle")))
|
||||||
|
void domSetTitle(const char* title, std::int32_t titleLen);
|
||||||
|
__attribute__((import_module("env"), import_name("domGetInnerWidth")))
|
||||||
|
std::int32_t domGetInnerWidth();
|
||||||
|
__attribute__((import_module("env"), import_name("domGetInnerHeight")))
|
||||||
|
std::int32_t domGetInnerHeight();
|
||||||
|
__attribute__((import_module("env"), import_name("domStartFrameLoop")))
|
||||||
|
void domStartFrameLoop();
|
||||||
|
__attribute__((import_module("env"), import_name("domStopFrameLoop")))
|
||||||
|
void domStopFrameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time string hash matching what dom-env.js sends through. The
|
||||||
|
// JS bridge marshals `KeyboardEvent.code` as a UTF-8 string; we hash it
|
||||||
|
// to a 32-bit KeyCode here so the same value compares equal against the
|
||||||
|
// table in :Keys (DOM branch). FNV-1a, deterministic, no allocation.
|
||||||
|
namespace {
|
||||||
|
constexpr KeyCode HashKeyCode(const char* p, std::size_t n) {
|
||||||
|
std::uint32_t h = 2166136261u;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
h ^= static_cast<std::uint8_t>(p[i]);
|
||||||
|
h *= 16777619u;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Window::Window(std::uint32_t w, std::uint32_t h, const std::string_view title)
|
||||||
|
: Window(w, h) {
|
||||||
|
SetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
Window::Window(std::uint32_t w, std::uint32_t h) : width(w), height(h) {
|
||||||
|
if (g_domWindow != nullptr) {
|
||||||
|
// Only one Window per page in V1. Subsequent constructions are
|
||||||
|
// a programming error — log loudly and clobber the previous
|
||||||
|
// pointer so the new Window's events at least fire.
|
||||||
|
// (stderr isn't reachable via `import std;` on wasi-sdk yet; just log
|
||||||
|
// to cout. The browser console pipes both to the same place.)
|
||||||
|
std::println("Crafter::Window: only one DOM Window per page; "
|
||||||
|
"overwriting the previous instance.");
|
||||||
|
}
|
||||||
|
g_domWindow = this;
|
||||||
|
|
||||||
|
// Use the browser-reported viewport size as the initial dimensions
|
||||||
|
// unless the caller asked for something specific. Browser owns the
|
||||||
|
// real size; w/h passed in are advisory.
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
width = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerWidth());
|
||||||
|
height = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handle passed to attach is just a non-zero token the JS side
|
||||||
|
// includes back in every dispatcher call. We don't use it on the
|
||||||
|
// C++ side (g_domWindow is the lookup) but it has to be non-zero so
|
||||||
|
// the JS bridge treats the window as "attached".
|
||||||
|
Crafter::DomEnv::domAttachWindow(1);
|
||||||
|
|
||||||
|
lastMousePos = {0, 0};
|
||||||
|
currentMousePos = {0, 0};
|
||||||
|
mouseDelta = {0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Window::~Window() {
|
||||||
|
// Clear the global pointer iff it still references us — defensive
|
||||||
|
// against a stack-allocated Window in main() that goes out of scope
|
||||||
|
// while rAF / DOM event callbacks are still queued. After this, the
|
||||||
|
// JS-side dispatchers (__crafterDom_*) early-return harmlessly. A
|
||||||
|
// shrill warning to the console flags the (almost certainly
|
||||||
|
// unintended) lifetime mistake so the user notices before everything
|
||||||
|
// mysteriously stops working.
|
||||||
|
if (g_domWindow == this) {
|
||||||
|
g_domWindow = nullptr;
|
||||||
|
std::println("Crafter::Window: destroyed while DOM mode is active. "
|
||||||
|
"Browser events will no-op until a new Window is constructed. "
|
||||||
|
"Did you forget to put the Window in `static` / `new`d storage?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::SetTitle(const std::string_view title) {
|
||||||
|
Crafter::DomEnv::domSetTitle(title.data(), static_cast<std::int32_t>(title.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
||||||
|
if (newWidth == 0 || newHeight == 0) return;
|
||||||
|
if (newWidth == width && newHeight == height) return;
|
||||||
|
width = newWidth;
|
||||||
|
height = newHeight;
|
||||||
|
onResize.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::SetCursorImage(std::uint16_t /*cw*/, std::uint16_t /*ch*/,
|
||||||
|
std::uint16_t /*hx*/, std::uint16_t /*hy*/,
|
||||||
|
const std::uint8_t* /*pixels*/) {
|
||||||
|
// V1: not wired. The natural impl is to base64-encode an inline PNG
|
||||||
|
// and assign it via document.body.style.cursor = `url(data:...) hx hy, auto`.
|
||||||
|
// Left for a follow-up so the first DOM build can ship without an
|
||||||
|
// inline PNG encoder.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::SetDefaultCursor() {
|
||||||
|
// Mirror SetCursorImage stub. Future impl: clear body.style.cursor.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::StartSync() {
|
||||||
|
// Hand the loop to rAF. Returns immediately; the wasm `_start`
|
||||||
|
// (main) finishes, and the runtime keeps the module alive while
|
||||||
|
// the JS-side rAF chain ticks `__crafterDom_frame`.
|
||||||
|
Crafter::DomEnv::domStartFrameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::StartUpdate() {
|
||||||
|
lastFrameBegin = std::chrono::high_resolution_clock::now();
|
||||||
|
updating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::StopUpdate() {
|
||||||
|
updating = false;
|
||||||
|
Crafter::DomEnv::domStopFrameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::Update() {
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
mouseDelta = {currentMousePos.x - lastMousePos.x,
|
||||||
|
currentMousePos.y - lastMousePos.y};
|
||||||
|
currentFrameTime = {now, now - lastFrameBegin};
|
||||||
|
onUpdate.Invoke(currentFrameTime);
|
||||||
|
lastMousePos = currentMousePos;
|
||||||
|
lastFrameBegin = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::Render() {
|
||||||
|
// V1: no rendering in DOM mode. Kept as a callable no-op so
|
||||||
|
// existing cross-platform code paths (e.g. main loops calling
|
||||||
|
// window.Render() before window.StartSync()) compile. V2 will
|
||||||
|
// hang the WebGPU command-submit here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── C exports the JS bridge calls back into ──────────────────────────
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
__attribute__((export_name("__crafterDom_frame")))
|
||||||
|
void __crafterDom_frame(std::int32_t /*handle*/) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
Gamepad::Tick();
|
||||||
|
g_domWindow->onBeforeUpdate.Invoke();
|
||||||
|
if (g_domWindow->updating) {
|
||||||
|
g_domWindow->Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_mouseMove")))
|
||||||
|
void __crafterDom_mouseMove(std::int32_t /*handle*/, double x, double y) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
g_domWindow->currentMousePos = {static_cast<float>(x), static_cast<float>(y)};
|
||||||
|
g_domWindow->onMouseMove.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_mouseDown")))
|
||||||
|
void __crafterDom_mouseDown(std::int32_t /*handle*/, std::int32_t button) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
// MouseEvent.button: 0=left, 1=middle, 2=right
|
||||||
|
if (button == 0) {
|
||||||
|
g_domWindow->mouseLeftHeld = true;
|
||||||
|
g_domWindow->onMouseLeftClick.Invoke();
|
||||||
|
} else if (button == 2) {
|
||||||
|
g_domWindow->mouseRightHeld = true;
|
||||||
|
g_domWindow->onMouseRightClick.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_mouseUp")))
|
||||||
|
void __crafterDom_mouseUp(std::int32_t /*handle*/, std::int32_t button) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
if (button == 0) {
|
||||||
|
g_domWindow->mouseLeftHeld = false;
|
||||||
|
g_domWindow->onMouseLeftRelease.Invoke();
|
||||||
|
} else if (button == 2) {
|
||||||
|
g_domWindow->mouseRightHeld = false;
|
||||||
|
g_domWindow->onMouseRightRelease.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_wheel")))
|
||||||
|
void __crafterDom_wheel(std::int32_t /*handle*/, double deltaY) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
|
||||||
|
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(deltaY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_keyDown")))
|
||||||
|
void __crafterDom_keyDown(std::int32_t /*handle*/,
|
||||||
|
const char* codePtr, std::int32_t codeLen,
|
||||||
|
const char* keyPtr, std::int32_t keyLen,
|
||||||
|
bool repeat) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
KeyCode code = HashKeyCode(codePtr, static_cast<std::size_t>(codeLen));
|
||||||
|
if (repeat) {
|
||||||
|
g_domWindow->onRawKeyHold.Invoke(code);
|
||||||
|
} else {
|
||||||
|
g_domWindow->heldKeys.insert(code);
|
||||||
|
g_domWindow->onRawKeyDown.Invoke(code);
|
||||||
|
}
|
||||||
|
// KeyboardEvent.key is the printable form. Forward as UTF-8
|
||||||
|
// text input for non-control keys so onTextInput drives input
|
||||||
|
// fields the same way it does on Win32 / Wayland.
|
||||||
|
if (keyLen == 1 && static_cast<unsigned char>(keyPtr[0]) >= 0x20
|
||||||
|
&& static_cast<unsigned char>(keyPtr[0]) != 0x7F) {
|
||||||
|
g_domWindow->onTextInput.Invoke(std::string_view(keyPtr, static_cast<std::size_t>(keyLen)));
|
||||||
|
} else if (keyLen > 1) {
|
||||||
|
// Multi-byte UTF-8 (non-ASCII printable). Forward as-is —
|
||||||
|
// dom-env.js always sends valid UTF-8.
|
||||||
|
g_domWindow->onTextInput.Invoke(std::string_view(keyPtr, static_cast<std::size_t>(keyLen)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_keyUp")))
|
||||||
|
void __crafterDom_keyUp(std::int32_t /*handle*/, const char* codePtr, std::int32_t codeLen) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
KeyCode code = HashKeyCode(codePtr, static_cast<std::size_t>(codeLen));
|
||||||
|
g_domWindow->heldKeys.erase(code);
|
||||||
|
g_domWindow->onRawKeyUp.Invoke(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_resize")))
|
||||||
|
void __crafterDom_resize(std::int32_t /*handle*/, std::int32_t newW, std::int32_t newH) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
g_domWindow->Resize(static_cast<std::uint32_t>(newW),
|
||||||
|
static_cast<std::uint32_t>(newH));
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("__crafterDom_close")))
|
||||||
|
void __crafterDom_close(std::int32_t /*handle*/) {
|
||||||
|
if (!g_domWindow) return;
|
||||||
|
g_domWindow->open = false;
|
||||||
|
g_domWindow->onClose.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:ComputeShader;
|
export module Crafter.Graphics:ComputeShader;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
|
|
||||||
|
|
@ -54,3 +57,4 @@ export namespace Crafter {
|
||||||
std::uint32_t gz = 1) const;
|
std::uint32_t gz = 1) const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:Decompress;
|
export module Crafter.Graphics:Decompress;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import :Device;
|
import :Device;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
|
|
@ -131,3 +134,4 @@ export namespace Crafter::Decompress {
|
||||||
vkCmdPipelineBarrier2(cmd, &dep);
|
vkCmdPipelineBarrier2(cmd, &dep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:DescriptorHeapVulkan;
|
export module Crafter.Graphics:DescriptorHeapVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :Window;
|
import :Window;
|
||||||
|
|
@ -327,3 +330,4 @@ export namespace Crafter {
|
||||||
std::uint16_t raw_ = 0;
|
std::uint16_t raw_ = 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
|
|
@ -53,6 +55,7 @@ export namespace Crafter {
|
||||||
struct Device {
|
struct Device {
|
||||||
static void Initialize();
|
static void Initialize();
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
inline static wl_display* display = nullptr;
|
inline static wl_display* display = nullptr;
|
||||||
inline static wl_seat* seat = nullptr;
|
inline static wl_seat* seat = nullptr;
|
||||||
|
|
@ -178,5 +181,13 @@ export namespace Crafter {
|
||||||
#else
|
#else
|
||||||
static void TickKeyRepeats() {}
|
static void TickKeyRepeats() {}
|
||||||
#endif
|
#endif
|
||||||
|
#else // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// DOM mode: Device collapses to just `Initialize()` (currently a
|
||||||
|
// no-op since the JS runtime initializes itself). The function is
|
||||||
|
// kept so user code calling `Device::Initialize()` still compiles
|
||||||
|
// cross-platform. Browser key repeat is delivered through the
|
||||||
|
// KeyboardEvent.repeat flag directly — no manual synthesis.
|
||||||
|
static void TickKeyRepeats() {}
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
217
interfaces/Crafter.Graphics-Dom.cppm
Normal file
217
interfaces/Crafter.Graphics-Dom.cppm
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Browser DOM bindings — absorbed from Crafter.CppDOM. Only meaningful
|
||||||
|
// when the build defined CRAFTER_GRAPHICS_WINDOW_DOM (i.e. a wasm32-*
|
||||||
|
// target paired with additional/dom-env.js); on native platforms the
|
||||||
|
// partition still parses but every method links to nothing because the
|
||||||
|
// build excludes the impl. project.cpp's per-target module set is what
|
||||||
|
// makes the partition disappear from the native libs.
|
||||||
|
//
|
||||||
|
// Two-class hierarchy (simplified from CppDOM's three-class design):
|
||||||
|
// HtmlElementPtr — non-owning element reference. Tracks every handler
|
||||||
|
// it registered and clears them on destruction, so
|
||||||
|
// the silent-leak bug from CppDOM's bare-pointer
|
||||||
|
// flavour can't happen. Releases the JS handle on
|
||||||
|
// destruction.
|
||||||
|
// HtmlElement — `HtmlElementPtr` plus element ownership: removes
|
||||||
|
// the element from the DOM on destruction. The new
|
||||||
|
// `Create` factory builds a fresh element under a
|
||||||
|
// parent and returns one of these.
|
||||||
|
//
|
||||||
|
// Move-only throughout. The JS handle (`ptr`) of a moved-from instance
|
||||||
|
// is zeroed so the destructor of the moved-from carcass is a no-op.
|
||||||
|
|
||||||
|
export module Crafter.Graphics:Dom;
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
import std;
|
||||||
|
import :DomEvents;
|
||||||
|
|
||||||
|
export namespace Crafter::Dom {
|
||||||
|
|
||||||
|
class HtmlElementPtr {
|
||||||
|
public:
|
||||||
|
// Opaque JS-side element cookie. 0 = "no element"; the destructor
|
||||||
|
// treats 0 as "moved-from, nothing to release".
|
||||||
|
std::int32_t ptr;
|
||||||
|
|
||||||
|
// Look up an existing element by its DOM id.
|
||||||
|
HtmlElementPtr(const std::string_view id);
|
||||||
|
// Look up + replace innerHTML in one step (matches CppDOM).
|
||||||
|
HtmlElementPtr(const std::string_view id, const std::string_view html);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Adopt a JS handle directly (used by HtmlElement::Create — the
|
||||||
|
// JS side has already minted the handle, no second lookup needed).
|
||||||
|
// Tagged-type ctor so the public id-based overloads stay
|
||||||
|
// unambiguous from the call site.
|
||||||
|
struct FromHandle { std::int32_t handle; };
|
||||||
|
explicit HtmlElementPtr(FromHandle h) noexcept : ptr(h.handle) {}
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
// Move-only — copying would silently double-free the JS handle.
|
||||||
|
HtmlElementPtr(HtmlElementPtr&&) noexcept;
|
||||||
|
HtmlElementPtr& operator=(HtmlElementPtr&&) noexcept;
|
||||||
|
HtmlElementPtr(const HtmlElementPtr&) = delete;
|
||||||
|
HtmlElementPtr& operator=(const HtmlElementPtr&) = delete;
|
||||||
|
|
||||||
|
~HtmlElementPtr();
|
||||||
|
|
||||||
|
// DOM ops ─────────────────────────────────────────────────────
|
||||||
|
void SetInnerHTML(const std::string_view html);
|
||||||
|
void SetStyle(const std::string_view style);
|
||||||
|
void SetProperty(const std::string_view property, const std::string_view value);
|
||||||
|
void AddClass(const std::string_view className);
|
||||||
|
void RemoveClass(const std::string_view className);
|
||||||
|
void ToggleClass(const std::string_view className);
|
||||||
|
bool HasClass(const std::string_view className);
|
||||||
|
std::string GetValue();
|
||||||
|
void SetValue(const std::string_view value);
|
||||||
|
|
||||||
|
// Listener API — each Add* returns an opaque id that can be
|
||||||
|
// passed to the matching Remove*. The destructor automatically
|
||||||
|
// removes every handler still registered, so manual removal is
|
||||||
|
// optional. Returns 0 only if registration failed at the JS
|
||||||
|
// boundary (the element was already collected). The 23 event
|
||||||
|
// types are 1:1 with CppDOM's surface.
|
||||||
|
std::int32_t AddClickListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveClickListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddMouseOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveMouseOverListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddMouseOutListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveMouseOutListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddMouseMoveListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveMouseMoveListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddMouseDownListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveMouseDownListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddMouseUpListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveMouseUpListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddFocusListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
||||||
|
void RemoveFocusListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddBlurListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
||||||
|
void RemoveBlurListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddKeyDownListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
||||||
|
void RemoveKeyDownListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddKeyUpListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
||||||
|
void RemoveKeyUpListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddKeyPressListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
||||||
|
void RemoveKeyPressListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddChangeListener(std::function<void(Crafter::Dom::ChangeEvent)> callback);
|
||||||
|
void RemoveChangeListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddSubmitListener(std::function<void()> callback);
|
||||||
|
void RemoveSubmitListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddInputListener(std::function<void(Crafter::Dom::InputEvent)> callback);
|
||||||
|
void RemoveInputListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddResizeListener(std::function<void(Crafter::Dom::ResizeEvent)> callback);
|
||||||
|
void RemoveResizeListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddScrollListener(std::function<void(Crafter::Dom::ScrollEvent)> callback);
|
||||||
|
void RemoveScrollListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddContextMenuListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveContextMenuListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDragStartListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDragStartListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDragEndListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDragEndListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDropListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDropListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDragOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDragOverListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDragEnterListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDragEnterListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddDragLeaveListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
||||||
|
void RemoveDragLeaveListener(std::int32_t id);
|
||||||
|
|
||||||
|
std::int32_t AddWheelListener(std::function<void(Crafter::Dom::WheelEvent)> callback);
|
||||||
|
void RemoveWheelListener(std::int32_t id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Per-event-kind handler-id lists. Each `Add*Listener` push_backs
|
||||||
|
// the id it returns; `RemoveAllHandlers` walks every list and
|
||||||
|
// calls the matching JS Remove on each id. The lists are indexed
|
||||||
|
// by event kind so a single handler-id collision across kinds
|
||||||
|
// (allowed by the JS bridge — counters are per-event-kind)
|
||||||
|
// doesn't cause a wrong-kind remove.
|
||||||
|
std::vector<std::int32_t> handlerIds_[24];
|
||||||
|
|
||||||
|
// Shared cleanup used by the destructor AND by move-assignment.
|
||||||
|
// Removes every registered handler from both the C++ map and
|
||||||
|
// the JS side, then frees the JS handle. Leaves `ptr == 0` on
|
||||||
|
// return so a second call is a no-op.
|
||||||
|
void RemoveAllHandlersAndFree();
|
||||||
|
};
|
||||||
|
|
||||||
|
class HtmlElement : public HtmlElementPtr {
|
||||||
|
public:
|
||||||
|
// Adopt an existing element by id; the dtor will remove it from
|
||||||
|
// the DOM. Useful when the caller created the element in markup
|
||||||
|
// and wants C++ to own its lifetime from now on.
|
||||||
|
HtmlElement(const std::string_view id);
|
||||||
|
HtmlElement(const std::string_view id, const std::string_view html);
|
||||||
|
|
||||||
|
HtmlElement(HtmlElement&&) noexcept;
|
||||||
|
HtmlElement& operator=(HtmlElement&&) noexcept;
|
||||||
|
|
||||||
|
~HtmlElement();
|
||||||
|
|
||||||
|
// Create a new element and append it to `parent`. Optional `id`
|
||||||
|
// sets the new element's DOM id (use it when you want to look the
|
||||||
|
// element up from non-C++ code later); empty leaves the element
|
||||||
|
// unnamed. Returns an owning handle — the element is removed from
|
||||||
|
// the DOM when this `HtmlElement` is destroyed.
|
||||||
|
static HtmlElement Create(const HtmlElementPtr& parent,
|
||||||
|
std::string_view tagName,
|
||||||
|
std::string_view id = {});
|
||||||
|
|
||||||
|
// Convenience: create a top-level element under document.body.
|
||||||
|
static HtmlElement CreateInBody(std::string_view tagName,
|
||||||
|
std::string_view id = {});
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Hidden ctor used by Create/CreateInBody — takes an already
|
||||||
|
// allocated JS handle via the base class FromHandle tag. Kept
|
||||||
|
// private so user code doesn't construct an HtmlElement from a
|
||||||
|
// raw handle by accident.
|
||||||
|
explicit HtmlElement(HtmlElementPtr::FromHandle h) noexcept
|
||||||
|
: HtmlElementPtr(h) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
119
interfaces/Crafter.Graphics-DomEvents.cppm
Normal file
119
interfaces/Crafter.Graphics-DomEvents.cppm
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DOM event POD structs delivered by dom-env.js into the per-listener
|
||||||
|
// callback set in :Dom. The set of fields and their semantics mirror the
|
||||||
|
// equivalent W3C event interfaces — the JS bridge marshals each field
|
||||||
|
// individually as a primitive over the wasm boundary (no struct passing,
|
||||||
|
// no JSON), so the layouts here are merely what the C++ side reconstructs
|
||||||
|
// before invoking the user's std::function.
|
||||||
|
|
||||||
|
export module Crafter.Graphics:DomEvents;
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
import std;
|
||||||
|
|
||||||
|
// The Dom* namespace mirrors CppDOM's flat `Crafter::*` event types but is
|
||||||
|
// re-homed under Crafter::Dom to avoid clashing with anything :Types or
|
||||||
|
// :Input may later add at the bare Crafter:: scope.
|
||||||
|
export namespace Crafter::Dom {
|
||||||
|
struct KeyboardEvent {
|
||||||
|
std::string key;
|
||||||
|
std::uint_fast8_t keyCode;
|
||||||
|
bool altKey;
|
||||||
|
bool ctrlKey;
|
||||||
|
bool shiftKey;
|
||||||
|
bool metaKey;
|
||||||
|
KeyboardEvent(const char* key, std::int32_t keyCode,
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey)
|
||||||
|
: key(key), keyCode(keyCode),
|
||||||
|
altKey(altKey), ctrlKey(ctrlKey),
|
||||||
|
shiftKey(shiftKey), metaKey(metaKey) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FocusEvent {
|
||||||
|
// Opaque JS handle cookies — round-trip back into env.js as the
|
||||||
|
// identity of the related element. No C++ API attaches semantics
|
||||||
|
// to them today; held for forward-compat with later helpers.
|
||||||
|
void* target;
|
||||||
|
void* relatedTarget;
|
||||||
|
FocusEvent(void* target, void* relatedTarget)
|
||||||
|
: target(target), relatedTarget(relatedTarget) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MouseEvent {
|
||||||
|
double clientX;
|
||||||
|
double clientY;
|
||||||
|
double screenX;
|
||||||
|
double screenY;
|
||||||
|
std::uint_fast32_t button;
|
||||||
|
std::uint_fast32_t buttons;
|
||||||
|
bool altKey;
|
||||||
|
bool ctrlKey;
|
||||||
|
bool shiftKey;
|
||||||
|
bool metaKey;
|
||||||
|
MouseEvent(double clientX, double clientY, double screenX, double screenY,
|
||||||
|
std::int32_t button, std::int32_t buttons,
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey)
|
||||||
|
: clientX(clientX), clientY(clientY),
|
||||||
|
screenX(screenX), screenY(screenY),
|
||||||
|
button(button), buttons(buttons),
|
||||||
|
altKey(altKey), ctrlKey(ctrlKey),
|
||||||
|
shiftKey(shiftKey), metaKey(metaKey) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputEvent {
|
||||||
|
std::string data;
|
||||||
|
bool isComposing;
|
||||||
|
InputEvent(const char* data, bool isComposing)
|
||||||
|
: data(data), isComposing(isComposing) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WheelEvent : public MouseEvent {
|
||||||
|
double deltaX;
|
||||||
|
double deltaY;
|
||||||
|
double deltaZ;
|
||||||
|
WheelEvent(double clientX, double clientY, double screenX, double screenY,
|
||||||
|
std::int32_t button, std::int32_t buttons,
|
||||||
|
bool altKey, bool ctrlKey, bool shiftKey, bool metaKey,
|
||||||
|
double deltaX, double deltaY, double deltaZ)
|
||||||
|
: MouseEvent(clientX, clientY, screenX, screenY,
|
||||||
|
button, buttons, altKey, ctrlKey, shiftKey, metaKey),
|
||||||
|
deltaX(deltaX), deltaY(deltaY), deltaZ(deltaZ) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResizeEvent {
|
||||||
|
std::uint_fast32_t width;
|
||||||
|
std::uint_fast32_t height;
|
||||||
|
ResizeEvent(std::uint_fast32_t width, std::uint_fast32_t height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScrollEvent {
|
||||||
|
double scrollX;
|
||||||
|
double scrollY;
|
||||||
|
ScrollEvent(double scrollX, double scrollY)
|
||||||
|
: scrollX(scrollX), scrollY(scrollY) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ChangeEvent {
|
||||||
|
std::string value;
|
||||||
|
ChangeEvent(const char* value) : value(value) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
@ -20,9 +20,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "../lib/stb_truetype.h"
|
#include "../lib/stb_truetype.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:Font;
|
export module Crafter.Graphics:Font;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
namespace Crafter {
|
namespace Crafter {
|
||||||
|
|
@ -68,3 +71,4 @@ namespace Crafter {
|
||||||
float ScaleForSize(float size); // stb's pixel-units-per-em factor
|
float ScaleForSize(float size); // stb's pixel-units-per-em factor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:FontAtlas;
|
export module Crafter.Graphics:FontAtlas;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Font;
|
import :Font;
|
||||||
import :ImageVulkan;
|
import :ImageVulkan;
|
||||||
|
|
@ -98,3 +101,4 @@ export namespace Crafter {
|
||||||
bool ShelfPlace(int w, int h, int& outX, int& outY);
|
bool ShelfPlace(int w, int h, int& outX, int& outY);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:ImageVulkan;
|
export module Crafter.Graphics:ImageVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Asset;
|
import Crafter.Asset;
|
||||||
import :Decompress;
|
import :Decompress;
|
||||||
|
|
@ -294,3 +297,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:InputField;
|
export module Crafter.Graphics:InputField;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Types;
|
import :Types;
|
||||||
import :Keys;
|
import :Keys;
|
||||||
|
|
@ -106,3 +110,4 @@ export namespace Crafter {
|
||||||
const InputFieldColors& colors,
|
const InputFieldColors& colors,
|
||||||
bool caretVisible);
|
bool caretVisible);
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,153 @@ export namespace Crafter {
|
||||||
case CrafterKeys::CrafterKeysMax: return 0;
|
case CrafterKeys::CrafterKeysMax: return 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// DOM mode: `KeyboardEvent.code` (a layout-independent physical-key
|
||||||
|
// string like "KeyA" or "Space") is hashed to a 32-bit KeyCode in
|
||||||
|
// dom-env.js and again here at compile time. FNV-1a was picked for
|
||||||
|
// its trivial implementation — collision chance over the ~110-entry
|
||||||
|
// key set is negligible.
|
||||||
|
constexpr auto fnv = [](const char* s) constexpr -> KeyCode {
|
||||||
|
std::uint32_t h = 2166136261u;
|
||||||
|
while (*s) {
|
||||||
|
h ^= static_cast<std::uint8_t>(*s++);
|
||||||
|
h *= 16777619u;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
switch (k) {
|
||||||
|
// Alphabetic — KeyboardEvent.code uses "Key" prefix.
|
||||||
|
case CrafterKeys::A: return fnv("KeyA");
|
||||||
|
case CrafterKeys::B: return fnv("KeyB");
|
||||||
|
case CrafterKeys::C: return fnv("KeyC");
|
||||||
|
case CrafterKeys::D: return fnv("KeyD");
|
||||||
|
case CrafterKeys::E: return fnv("KeyE");
|
||||||
|
case CrafterKeys::F: return fnv("KeyF");
|
||||||
|
case CrafterKeys::G: return fnv("KeyG");
|
||||||
|
case CrafterKeys::H: return fnv("KeyH");
|
||||||
|
case CrafterKeys::I: return fnv("KeyI");
|
||||||
|
case CrafterKeys::J: return fnv("KeyJ");
|
||||||
|
case CrafterKeys::K: return fnv("KeyK");
|
||||||
|
case CrafterKeys::L: return fnv("KeyL");
|
||||||
|
case CrafterKeys::M: return fnv("KeyM");
|
||||||
|
case CrafterKeys::N: return fnv("KeyN");
|
||||||
|
case CrafterKeys::O: return fnv("KeyO");
|
||||||
|
case CrafterKeys::P: return fnv("KeyP");
|
||||||
|
case CrafterKeys::Q: return fnv("KeyQ");
|
||||||
|
case CrafterKeys::R: return fnv("KeyR");
|
||||||
|
case CrafterKeys::S: return fnv("KeyS");
|
||||||
|
case CrafterKeys::T: return fnv("KeyT");
|
||||||
|
case CrafterKeys::U: return fnv("KeyU");
|
||||||
|
case CrafterKeys::V: return fnv("KeyV");
|
||||||
|
case CrafterKeys::W: return fnv("KeyW");
|
||||||
|
case CrafterKeys::X: return fnv("KeyX");
|
||||||
|
case CrafterKeys::Y: return fnv("KeyY");
|
||||||
|
case CrafterKeys::Z: return fnv("KeyZ");
|
||||||
|
// Numeric (top row) — DigitN.
|
||||||
|
case CrafterKeys::_0: return fnv("Digit0");
|
||||||
|
case CrafterKeys::_1: return fnv("Digit1");
|
||||||
|
case CrafterKeys::_2: return fnv("Digit2");
|
||||||
|
case CrafterKeys::_3: return fnv("Digit3");
|
||||||
|
case CrafterKeys::_4: return fnv("Digit4");
|
||||||
|
case CrafterKeys::_5: return fnv("Digit5");
|
||||||
|
case CrafterKeys::_6: return fnv("Digit6");
|
||||||
|
case CrafterKeys::_7: return fnv("Digit7");
|
||||||
|
case CrafterKeys::_8: return fnv("Digit8");
|
||||||
|
case CrafterKeys::_9: return fnv("Digit9");
|
||||||
|
// Function keys.
|
||||||
|
case CrafterKeys::F1: return fnv("F1");
|
||||||
|
case CrafterKeys::F2: return fnv("F2");
|
||||||
|
case CrafterKeys::F3: return fnv("F3");
|
||||||
|
case CrafterKeys::F4: return fnv("F4");
|
||||||
|
case CrafterKeys::F5: return fnv("F5");
|
||||||
|
case CrafterKeys::F6: return fnv("F6");
|
||||||
|
case CrafterKeys::F7: return fnv("F7");
|
||||||
|
case CrafterKeys::F8: return fnv("F8");
|
||||||
|
case CrafterKeys::F9: return fnv("F9");
|
||||||
|
case CrafterKeys::F10: return fnv("F10");
|
||||||
|
case CrafterKeys::F11: return fnv("F11");
|
||||||
|
case CrafterKeys::F12: return fnv("F12");
|
||||||
|
// Control keys.
|
||||||
|
case CrafterKeys::Escape: return fnv("Escape");
|
||||||
|
case CrafterKeys::Tab: return fnv("Tab");
|
||||||
|
case CrafterKeys::Enter: return fnv("Enter");
|
||||||
|
case CrafterKeys::Space: return fnv("Space");
|
||||||
|
case CrafterKeys::Backspace: return fnv("Backspace");
|
||||||
|
case CrafterKeys::Delete: return fnv("Delete");
|
||||||
|
case CrafterKeys::Insert: return fnv("Insert");
|
||||||
|
case CrafterKeys::Home: return fnv("Home");
|
||||||
|
case CrafterKeys::End: return fnv("End");
|
||||||
|
case CrafterKeys::PageUp: return fnv("PageUp");
|
||||||
|
case CrafterKeys::PageDown: return fnv("PageDown");
|
||||||
|
case CrafterKeys::CapsLock: return fnv("CapsLock");
|
||||||
|
case CrafterKeys::NumLock: return fnv("NumLock");
|
||||||
|
case CrafterKeys::ScrollLock: return fnv("ScrollLock");
|
||||||
|
// Modifiers.
|
||||||
|
case CrafterKeys::LeftShift: return fnv("ShiftLeft");
|
||||||
|
case CrafterKeys::RightShift: return fnv("ShiftRight");
|
||||||
|
case CrafterKeys::LeftCtrl: return fnv("ControlLeft");
|
||||||
|
case CrafterKeys::RightCtrl: return fnv("ControlRight");
|
||||||
|
case CrafterKeys::LeftAlt: return fnv("AltLeft");
|
||||||
|
case CrafterKeys::RightAlt: return fnv("AltRight");
|
||||||
|
case CrafterKeys::LeftSuper: return fnv("MetaLeft");
|
||||||
|
case CrafterKeys::RightSuper: return fnv("MetaRight");
|
||||||
|
// Arrows.
|
||||||
|
case CrafterKeys::Up: return fnv("ArrowUp");
|
||||||
|
case CrafterKeys::Down: return fnv("ArrowDown");
|
||||||
|
case CrafterKeys::Left: return fnv("ArrowLeft");
|
||||||
|
case CrafterKeys::Right: return fnv("ArrowRight");
|
||||||
|
// Keypad.
|
||||||
|
case CrafterKeys::keypad_0: return fnv("Numpad0");
|
||||||
|
case CrafterKeys::keypad_1: return fnv("Numpad1");
|
||||||
|
case CrafterKeys::keypad_2: return fnv("Numpad2");
|
||||||
|
case CrafterKeys::keypad_3: return fnv("Numpad3");
|
||||||
|
case CrafterKeys::keypad_4: return fnv("Numpad4");
|
||||||
|
case CrafterKeys::keypad_5: return fnv("Numpad5");
|
||||||
|
case CrafterKeys::keypad_6: return fnv("Numpad6");
|
||||||
|
case CrafterKeys::keypad_7: return fnv("Numpad7");
|
||||||
|
case CrafterKeys::keypad_8: return fnv("Numpad8");
|
||||||
|
case CrafterKeys::keypad_9: return fnv("Numpad9");
|
||||||
|
case CrafterKeys::keypad_enter: return fnv("NumpadEnter");
|
||||||
|
case CrafterKeys::keypad_plus: return fnv("NumpadAdd");
|
||||||
|
case CrafterKeys::keypad_minus: return fnv("NumpadSubtract");
|
||||||
|
case CrafterKeys::keypad_multiply: return fnv("NumpadMultiply");
|
||||||
|
case CrafterKeys::keypad_divide: return fnv("NumpadDivide");
|
||||||
|
case CrafterKeys::keypad_decimal: return fnv("NumpadDecimal");
|
||||||
|
// Punctuation.
|
||||||
|
case CrafterKeys::grave: return fnv("Backquote");
|
||||||
|
case CrafterKeys::minus: return fnv("Minus");
|
||||||
|
case CrafterKeys::equal: return fnv("Equal");
|
||||||
|
case CrafterKeys::bracket_left: return fnv("BracketLeft");
|
||||||
|
case CrafterKeys::bracket_right: return fnv("BracketRight");
|
||||||
|
case CrafterKeys::backslash: return fnv("Backslash");
|
||||||
|
case CrafterKeys::semicolon: return fnv("Semicolon");
|
||||||
|
case CrafterKeys::quote: return fnv("Quote");
|
||||||
|
case CrafterKeys::comma: return fnv("Comma");
|
||||||
|
case CrafterKeys::period: return fnv("Period");
|
||||||
|
case CrafterKeys::slash: return fnv("Slash");
|
||||||
|
case CrafterKeys::print_screen: return fnv("PrintScreen");
|
||||||
|
case CrafterKeys::pause: return fnv("Pause");
|
||||||
|
case CrafterKeys::menu: return fnv("ContextMenu");
|
||||||
|
// Multimedia / browser / launch.
|
||||||
|
case CrafterKeys::volume_up: return fnv("AudioVolumeUp");
|
||||||
|
case CrafterKeys::volume_down: return fnv("AudioVolumeDown");
|
||||||
|
case CrafterKeys::volume_mute: return fnv("AudioVolumeMute");
|
||||||
|
case CrafterKeys::media_play: return fnv("MediaPlayPause");
|
||||||
|
case CrafterKeys::media_stop: return fnv("MediaStop");
|
||||||
|
case CrafterKeys::media_prev: return fnv("MediaTrackPrevious");
|
||||||
|
case CrafterKeys::media_next: return fnv("MediaTrackNext");
|
||||||
|
case CrafterKeys::browser_back: return fnv("BrowserBack");
|
||||||
|
case CrafterKeys::browser_forward: return fnv("BrowserForward");
|
||||||
|
case CrafterKeys::browser_refresh: return fnv("BrowserRefresh");
|
||||||
|
case CrafterKeys::browser_stop: return fnv("BrowserStop");
|
||||||
|
case CrafterKeys::browser_search: return fnv("BrowserSearch");
|
||||||
|
case CrafterKeys::browser_home: return fnv("BrowserHome");
|
||||||
|
case CrafterKeys::launch_mail: return fnv("LaunchMail");
|
||||||
|
case CrafterKeys::launch_calculator: return fnv("LaunchApp2");
|
||||||
|
case CrafterKeys::launch_media_player: return fnv("LaunchApp1");
|
||||||
|
case CrafterKeys::CrafterKeysMax: return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
// Linux kernel input-event-codes. The Wayland keyboard handler strips
|
// Linux kernel input-event-codes. The Wayland keyboard handler strips
|
||||||
// the +8 X11 offset before delivery so these match wl_keyboard.key
|
// the +8 X11 offset before delivery so these match wl_keyboard.key
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:Mesh;
|
export module Crafter.Graphics:Mesh;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Math;
|
import Crafter.Math;
|
||||||
import Crafter.Asset;
|
import Crafter.Asset;
|
||||||
|
|
@ -56,3 +59,4 @@ export namespace Crafter {
|
||||||
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd);
|
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:PipelineRTVulkan;
|
export module Crafter.Graphics:PipelineRTVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :VulkanBuffer;
|
import :VulkanBuffer;
|
||||||
|
|
@ -113,3 +116,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:RTPass;
|
export module Crafter.Graphics:RTPass;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :RenderPass;
|
import :RenderPass;
|
||||||
import :Window;
|
import :Window;
|
||||||
|
|
@ -42,3 +45,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:RenderPass;
|
export module Crafter.Graphics:RenderPass;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
|
|
@ -29,3 +32,4 @@ export namespace Crafter {
|
||||||
virtual ~RenderPass() = default;
|
virtual ~RenderPass() = default;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:RenderingElement3D;
|
export module Crafter.Graphics:RenderingElement3D;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Mesh;
|
import :Mesh;
|
||||||
import :VulkanBuffer;
|
import :VulkanBuffer;
|
||||||
|
|
@ -83,3 +86,4 @@ export namespace Crafter {
|
||||||
static void Remove(RenderingElement3D* e);
|
static void Remove(RenderingElement3D* e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
50
interfaces/Crafter.Graphics-Router.cppm
Normal file
50
interfaces/Crafter.Graphics-Router.cppm
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// History / SPA routing. Thin C++ wrapper over the browser's
|
||||||
|
// `history.pushState` / `popstate` and `window.location.pathname` —
|
||||||
|
// exists so a single-page app written against this library can manage
|
||||||
|
// its own URL without going through the DOM partition. Re-homed under
|
||||||
|
// the `Crafter::Router` namespace (cleaner than CppDOM's bare free
|
||||||
|
// functions; symmetric with `Crafter::Gamepad`).
|
||||||
|
|
||||||
|
export module Crafter.Graphics:Router;
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace Crafter::Router {
|
||||||
|
|
||||||
|
// Push a new history entry. `data` is a JSON string serialized by
|
||||||
|
// the caller — the browser stores it on the entry but the popstate
|
||||||
|
// listener in V1 receives no payload (matches CppDOM's surface).
|
||||||
|
// `url` is browser-relative, e.g. "/blog/post-1".
|
||||||
|
void PushState(std::string_view data, std::string_view title, std::string_view url);
|
||||||
|
|
||||||
|
// Subscribe to the browser's `popstate` event (back/forward button,
|
||||||
|
// programmatic history.go). Returns an opaque id usable with
|
||||||
|
// `RemovePopStateListener`. Multiple subscribers OK.
|
||||||
|
std::int32_t AddPopStateListener(std::function<void()> callback);
|
||||||
|
void RemovePopStateListener(std::int32_t id);
|
||||||
|
|
||||||
|
// Current `window.location.pathname` as a freshly-allocated string.
|
||||||
|
// Allocates per call — cache the result in the caller if used in
|
||||||
|
// hot paths.
|
||||||
|
std::string GetPath();
|
||||||
|
}
|
||||||
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
@ -19,9 +19,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:SamplerVulkan;
|
export module Crafter.Graphics:SamplerVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :VulkanBuffer;
|
import :VulkanBuffer;
|
||||||
import :ImageVulkan;
|
import :ImageVulkan;
|
||||||
|
|
@ -58,3 +61,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:ShaderBindingTableVulkan;
|
export module Crafter.Graphics:ShaderBindingTableVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :VulkanBuffer;
|
import :VulkanBuffer;
|
||||||
|
|
@ -38,3 +41,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:ShaderVulkan;
|
export module Crafter.Graphics:ShaderVulkan;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :Types;
|
import :Types;
|
||||||
|
|
@ -60,3 +63,4 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
// DOM builds drop the Vulkan header — DescriptorBinding (the only
|
||||||
|
// Vk-typed entity here) compiles out under the same guard below.
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif
|
||||||
export module Crafter.Graphics:Types;
|
export module Crafter.Graphics:Types;
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Math;
|
import Crafter.Math;
|
||||||
|
|
@ -245,8 +249,10 @@ export namespace Crafter {
|
||||||
return std::tan(fov * std::numbers::pi / 360.0);
|
return std::tan(fov * std::numbers::pi / 360.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
struct DescriptorBinding {
|
struct DescriptorBinding {
|
||||||
VkDescriptorType type;
|
VkDescriptorType type;
|
||||||
std::uint32_t slot;
|
std::uint32_t slot;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:UI;
|
export module Crafter.Graphics:UI;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Event;
|
import Crafter.Event;
|
||||||
import :Device;
|
import :Device;
|
||||||
|
|
@ -336,3 +339,4 @@ export namespace Crafter {
|
||||||
return ImageSlot{heap_, range.firstElement};
|
return ImageSlot{heap_, range.firstElement};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:UIComponents;
|
export module Crafter.Graphics:UIComponents;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :UI;
|
import :UI;
|
||||||
import :Font;
|
import :Font;
|
||||||
|
|
@ -140,3 +143,4 @@ export namespace Crafter {
|
||||||
std::array<float, 4> tint = {1, 1, 1, 1},
|
std::array<float, 4> tint = {1, 1, 1, 1},
|
||||||
std::array<float, 4> uv = {0, 0, 1, 1});
|
std::array<float, 4> uv = {0, 0, 1, 1});
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:VulkanBuffer;
|
export module Crafter.Graphics:VulkanBuffer;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
import :Device;
|
import :Device;
|
||||||
|
|
||||||
|
|
@ -219,3 +222,4 @@ namespace Crafter {
|
||||||
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
|
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:VulkanTransition;
|
export module Crafter.Graphics:VulkanTransition;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
|
|
@ -187,3 +190,4 @@ export namespace Crafter {
|
||||||
image_memory_barriers.data());
|
image_memory_barriers.data());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ module;
|
||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -50,6 +52,7 @@ import :Keys;
|
||||||
import Crafter.Event;
|
import Crafter.Event;
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
struct Semaphores {
|
struct Semaphores {
|
||||||
// Swap chain image presentation
|
// Swap chain image presentation
|
||||||
VkSemaphore presentComplete;
|
VkSemaphore presentComplete;
|
||||||
|
|
@ -58,6 +61,7 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
struct RenderPass;
|
struct RenderPass;
|
||||||
struct DescriptorHeapVulkan;
|
struct DescriptorHeapVulkan;
|
||||||
|
#endif
|
||||||
|
|
||||||
struct Window {
|
struct Window {
|
||||||
FrameTime currentFrameTime;
|
FrameTime currentFrameTime;
|
||||||
|
|
@ -105,6 +109,16 @@ export namespace Crafter {
|
||||||
Window(Window&) = delete;
|
Window(Window&) = delete;
|
||||||
Window(Window&&) = delete;
|
Window(Window&&) = delete;
|
||||||
Window& operator=(const Window&) = delete;
|
Window& operator=(const Window&) = delete;
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// DOM mode keeps a process-global pointer to the live Window so
|
||||||
|
// the JS bridge can deliver document/window events back. If the
|
||||||
|
// Window is destroyed (e.g. it was stack-allocated in main and
|
||||||
|
// main returned), the global must be cleared so subsequent
|
||||||
|
// browser callbacks become no-ops instead of dereferencing
|
||||||
|
// freed memory. Native builds have nothing to clean up at the
|
||||||
|
// Window level (Vulkan resources teardown is the user's job).
|
||||||
|
~Window();
|
||||||
|
#endif
|
||||||
|
|
||||||
void StartSync();
|
void StartSync();
|
||||||
void StartUpdate();
|
void StartUpdate();
|
||||||
|
|
@ -193,6 +207,7 @@ export namespace Crafter {
|
||||||
inline static wp_fractional_scale_v1* wp_scale = nullptr;
|
inline static wp_fractional_scale_v1* wp_scale = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
VkCommandBuffer StartInit();
|
VkCommandBuffer StartInit();
|
||||||
void FinishInit();
|
void FinishInit();
|
||||||
VkCommandBuffer GetCmd();
|
VkCommandBuffer GetCmd();
|
||||||
|
|
@ -226,5 +241,13 @@ export namespace Crafter {
|
||||||
std::vector<RenderPass*> passes;
|
std::vector<RenderPass*> passes;
|
||||||
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
||||||
std::optional<std::array<float, 4>> clearColor;
|
std::optional<std::array<float, 4>> clearColor;
|
||||||
|
#else
|
||||||
|
// DOM mode: the page IS the window. `numFrames` stays as a public
|
||||||
|
// constant so cross-platform code can refer to Window::numFrames
|
||||||
|
// without #ifdef'ing the reference; nothing else lives here yet.
|
||||||
|
// V2 (WebGPU compute) will hang its GPUContext / swapchain texture
|
||||||
|
// members off this branch.
|
||||||
|
static constexpr std::uint8_t numFrames = 1;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -20,16 +20,25 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
export module Crafter.Graphics;
|
export module Crafter.Graphics;
|
||||||
|
|
||||||
|
// Every partition is `export import`'d unconditionally. Crafter.Build's
|
||||||
|
// dependency scanner doesn't respect `#ifdef` on `import :X`, so the
|
||||||
|
// partition file must always exist. Vulkan-typed partitions stub to
|
||||||
|
// empty modules under CRAFTER_GRAPHICS_WINDOW_DOM, and the new Dom /
|
||||||
|
// DomEvents / Router partitions stub to empty modules in native
|
||||||
|
// builds — see project.cpp for the master list.
|
||||||
export import :Types;
|
export import :Types;
|
||||||
export import :Keys;
|
export import :Keys;
|
||||||
export import :Gamepad;
|
export import :Gamepad;
|
||||||
export import :Window;
|
export import :Window;
|
||||||
export import :Input;
|
export import :Input;
|
||||||
export import :Device;
|
export import :Device;
|
||||||
export import :Font;
|
|
||||||
export import :Animation;
|
export import :Animation;
|
||||||
export import :Mesh;
|
|
||||||
export import :ForwardDeclarations;
|
export import :ForwardDeclarations;
|
||||||
|
export import :Clipboard;
|
||||||
|
|
||||||
|
// Vulkan-backed partitions — empty under DOM.
|
||||||
|
export import :Font;
|
||||||
|
export import :Mesh;
|
||||||
export import :VulkanTransition;
|
export import :VulkanTransition;
|
||||||
export import :VulkanBuffer;
|
export import :VulkanBuffer;
|
||||||
export import :ShaderVulkan;
|
export import :ShaderVulkan;
|
||||||
|
|
@ -46,5 +55,9 @@ export import :ComputeShader;
|
||||||
export import :UI;
|
export import :UI;
|
||||||
export import :UIComponents;
|
export import :UIComponents;
|
||||||
export import :InputField;
|
export import :InputField;
|
||||||
export import :Clipboard;
|
|
||||||
export import :Decompress;
|
export import :Decompress;
|
||||||
|
|
||||||
|
// DOM-only partitions — empty under native.
|
||||||
|
export import :Dom;
|
||||||
|
export import :DomEvents;
|
||||||
|
export import :Router;
|
||||||
|
|
|
||||||
81
project.cpp
81
project.cpp
|
|
@ -31,9 +31,23 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sniff the requested target from args before any deps resolve — the
|
||||||
|
// Crafter.Asset dependency is heavy and not wasm-ready (uses `throw`
|
||||||
|
// under -fno-exceptions, references `_Float16`). The DOM build stubs
|
||||||
|
// the renderer entirely so the dep doesn't apply anyway.
|
||||||
|
bool isWasm = false;
|
||||||
|
for (std::string_view a : args) {
|
||||||
|
if (a.starts_with("--target=") && a.find("wasm") != std::string_view::npos) {
|
||||||
|
isWasm = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Configuration* event = resolveDep("Crafter.Event", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git");
|
Configuration* event = resolveDep("Crafter.Event", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git");
|
||||||
Configuration* math = resolveDep("Crafter.Math", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git");
|
Configuration* math = resolveDep("Crafter.Math", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git");
|
||||||
Configuration* asset = resolveDep("Crafter.Asset", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git");
|
Configuration* asset = isWasm
|
||||||
|
? nullptr
|
||||||
|
: resolveDep("Crafter.Asset", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git");
|
||||||
|
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
|
|
@ -41,15 +55,30 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.outputName = "Crafter.Graphics";
|
cfg.outputName = "Crafter.Graphics";
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
cfg.type = ConfigurationType::LibraryStatic;
|
||||||
auto opts = ApplyStandardArgs(cfg, args);
|
auto opts = ApplyStandardArgs(cfg, args);
|
||||||
|
if (asset) {
|
||||||
cfg.dependencies = { event, math, asset };
|
cfg.dependencies = { event, math, asset };
|
||||||
|
} else {
|
||||||
|
cfg.dependencies = { event, math };
|
||||||
|
}
|
||||||
|
|
||||||
// Window backend follows the target triple. V1 had separate lib-wayland /
|
// Window backend follows the target triple. V1 had separate lib-wayland /
|
||||||
// lib-win32 configurations; V2 picks the right one automatically based on
|
// lib-win32 configurations; V2 picks the right one automatically based on
|
||||||
// where the build is going. Cross-compile (`--target=...`) flips the
|
// where the build is going. Cross-compile (`--target=...`) flips the
|
||||||
// backend along with everything else.
|
// backend along with everything else. The DOM backend is reached by any
|
||||||
bool windows = cfg.target.find("windows") != std::string::npos
|
// wasm32-* target and produces a Vulkan-free build whose Window is wired
|
||||||
|| cfg.target.find("mingw") != std::string::npos;
|
// to a custom JS env (see additional/dom-env.js).
|
||||||
if (windows) {
|
bool dom = cfg.target.find("wasm") != std::string::npos;
|
||||||
|
bool windows = !dom
|
||||||
|
&& (cfg.target.find("windows") != std::string::npos
|
||||||
|
|| cfg.target.find("mingw") != std::string::npos);
|
||||||
|
if (dom) {
|
||||||
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
|
||||||
|
// No native window libs, no Vulkan loader, no Wayland/X11. The JS
|
||||||
|
// bridge satisfies every dynamic symbol via wasm imports. Crafter.Build
|
||||||
|
// strips -march/-mtune from the clang command line for any wasm32-*
|
||||||
|
// triple, so cfg.march/mtune can stay at their defaults — keeping them
|
||||||
|
// matches the VariantId of dependency PCMs.
|
||||||
|
} else if (windows) {
|
||||||
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WIN32", ""});
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WIN32", ""});
|
||||||
cfg.linkFlags.push_back("-lkernel32");
|
cfg.linkFlags.push_back("-lkernel32");
|
||||||
cfg.linkFlags.push_back("-luser32");
|
cfg.linkFlags.push_back("-luser32");
|
||||||
|
|
@ -74,8 +103,13 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.cFiles.push_back("lib/viewporter");
|
cfg.cFiles.push_back("lib/viewporter");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vulkan is the only renderer. Software fallback is provided externally
|
// Vulkan is the only renderer on native targets. Software fallback is
|
||||||
// via the Vulkan loader (e.g. llvmpipe / lavapipe) — no separate code path.
|
// provided externally via the Vulkan loader (e.g. llvmpipe / lavapipe) —
|
||||||
|
// no separate code path. The DOM backend doesn't render in V1 (the
|
||||||
|
// UIRenderer and every Vulkan-typed module are excluded below); the
|
||||||
|
// WebGPU follow-up will gain its own headers/loader rather than reuse
|
||||||
|
// the Vulkan ones.
|
||||||
|
if (!dom) {
|
||||||
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
|
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
|
||||||
vkHeaders.name = "Vulkan-Headers";
|
vkHeaders.name = "Vulkan-Headers";
|
||||||
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
|
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
|
||||||
|
|
@ -87,10 +121,17 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
vkUtility.builder = ExternalBuilder::None;
|
vkUtility.builder = ExternalBuilder::None;
|
||||||
vkUtility.includeDirs = { "include" };
|
vkUtility.includeDirs = { "include" };
|
||||||
cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
|
cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
||||||
|
|
||||||
std::array<fs::path, 29> ifaces = {
|
// One master interface list. Every partition exists on every target
|
||||||
|
// — Crafter.Build's dependency scanner doesn't respect `#ifdef` on
|
||||||
|
// `import :X` statements, so the partition file must be present even
|
||||||
|
// when its body is gated out. Vulkan-typed partitions stub to empty
|
||||||
|
// modules under CRAFTER_GRAPHICS_WINDOW_DOM; the Dom/DomEvents/Router
|
||||||
|
// partitions stub to empty modules in the opposite direction.
|
||||||
|
std::array<fs::path, 32> ifaces = {
|
||||||
"interfaces/Crafter.Graphics",
|
"interfaces/Crafter.Graphics",
|
||||||
"interfaces/Crafter.Graphics-Animation",
|
"interfaces/Crafter.Graphics-Animation",
|
||||||
"interfaces/Crafter.Graphics-Clipboard",
|
"interfaces/Crafter.Graphics-Clipboard",
|
||||||
|
|
@ -98,6 +139,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-Decompress",
|
"interfaces/Crafter.Graphics-Decompress",
|
||||||
"interfaces/Crafter.Graphics-DescriptorHeapVulkan",
|
"interfaces/Crafter.Graphics-DescriptorHeapVulkan",
|
||||||
"interfaces/Crafter.Graphics-Device",
|
"interfaces/Crafter.Graphics-Device",
|
||||||
|
"interfaces/Crafter.Graphics-Dom",
|
||||||
|
"interfaces/Crafter.Graphics-DomEvents",
|
||||||
"interfaces/Crafter.Graphics-Font",
|
"interfaces/Crafter.Graphics-Font",
|
||||||
"interfaces/Crafter.Graphics-FontAtlas",
|
"interfaces/Crafter.Graphics-FontAtlas",
|
||||||
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
||||||
|
|
@ -110,6 +153,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
||||||
"interfaces/Crafter.Graphics-RenderingElement3D",
|
"interfaces/Crafter.Graphics-RenderingElement3D",
|
||||||
"interfaces/Crafter.Graphics-RenderPass",
|
"interfaces/Crafter.Graphics-RenderPass",
|
||||||
|
"interfaces/Crafter.Graphics-Router",
|
||||||
"interfaces/Crafter.Graphics-RTPass",
|
"interfaces/Crafter.Graphics-RTPass",
|
||||||
"interfaces/Crafter.Graphics-SamplerVulkan",
|
"interfaces/Crafter.Graphics-SamplerVulkan",
|
||||||
"interfaces/Crafter.Graphics-ShaderBindingTableVulkan",
|
"interfaces/Crafter.Graphics-ShaderBindingTableVulkan",
|
||||||
|
|
@ -121,6 +165,26 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-VulkanTransition",
|
"interfaces/Crafter.Graphics-VulkanTransition",
|
||||||
"interfaces/Crafter.Graphics-Window",
|
"interfaces/Crafter.Graphics-Window",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (dom) {
|
||||||
|
// DOM impl set: only the files whose bodies do meaningful work
|
||||||
|
// under CRAFTER_GRAPHICS_WINDOW_DOM. Vulkan-only impls (Mesh,
|
||||||
|
// ComputeShader, Font, etc.) stay out — their interface stubs
|
||||||
|
// mean no link-side references; including the impl would just
|
||||||
|
// be dead code with no Vulkan headers to compile against.
|
||||||
|
std::array<fs::path, 6> domImpls = {
|
||||||
|
"implementations/Crafter.Graphics-Clipboard",
|
||||||
|
"implementations/Crafter.Graphics-Dom",
|
||||||
|
"implementations/Crafter.Graphics-Gamepad",
|
||||||
|
"implementations/Crafter.Graphics-Input",
|
||||||
|
"implementations/Crafter.Graphics-Router",
|
||||||
|
"implementations/Crafter.Graphics-Window",
|
||||||
|
};
|
||||||
|
cfg.GetInterfacesAndImplementations(ifaces, domImpls);
|
||||||
|
// JS glue shipped alongside the .wasm so the loader has the
|
||||||
|
// env-import surface the Window/Dom bindings expect.
|
||||||
|
cfg.files.emplace_back(fs::path("additional/dom-env.js"));
|
||||||
|
} else {
|
||||||
std::array<fs::path, 13> impls = {
|
std::array<fs::path, 13> impls = {
|
||||||
"implementations/Crafter.Graphics-Clipboard",
|
"implementations/Crafter.Graphics-Clipboard",
|
||||||
"implementations/Crafter.Graphics-ComputeShader",
|
"implementations/Crafter.Graphics-ComputeShader",
|
||||||
|
|
@ -143,6 +207,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute);
|
cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute);
|
cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
||||||
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue