wasm improvements
This commit is contained in:
parent
42a479572d
commit
6aa0338c0f
9 changed files with 603 additions and 70 deletions
|
|
@ -109,10 +109,60 @@ function setStyle(cookie, stylePtr, styleLen) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
if (el) el.style.cssText = __readUtf8(stylePtr, styleLen);
|
if (el) el.style.cssText = __readUtf8(stylePtr, styleLen);
|
||||||
}
|
}
|
||||||
|
// NOTE: setProperty is a CSS custom-property setter (el.style.setProperty),
|
||||||
|
// NOT setAttribute. The name is historical and misleading; the attribute
|
||||||
|
// functions below are the ones you want for href/src/disabled/aria-*.
|
||||||
function setProperty(cookie, propPtr, propLen, valPtr, valLen) {
|
function setProperty(cookie, propPtr, propLen, valPtr, valLen) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
if (el) el.style.setProperty(__readUtf8(propPtr, propLen), __readUtf8(valPtr, valLen));
|
if (el) el.style.setProperty(__readUtf8(propPtr, propLen), __readUtf8(valPtr, valLen));
|
||||||
}
|
}
|
||||||
|
// Real attribute access. Without these the only way to change an href, src,
|
||||||
|
// disabled or aria-* was to re-render the parent's innerHTML, which destroys
|
||||||
|
// every descendant (and their event listeners) to change one string.
|
||||||
|
function setAttribute(cookie, namePtr, nameLen, valPtr, valLen) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (el) el.setAttribute(__readUtf8(namePtr, nameLen), __readUtf8(valPtr, valLen));
|
||||||
|
}
|
||||||
|
function removeAttribute(cookie, namePtr, nameLen) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (el) el.removeAttribute(__readUtf8(namePtr, nameLen));
|
||||||
|
}
|
||||||
|
// Returns 0 for "absent" so the caller can tell it apart from an attribute
|
||||||
|
// that is present with an empty value (`disabled=""`, `alt=""`) — which
|
||||||
|
// matters because for boolean attributes presence alone is the signal.
|
||||||
|
function getAttribute(cookie, namePtr, nameLen) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (!el) return 0;
|
||||||
|
const v = el.getAttribute(__readUtf8(namePtr, nameLen));
|
||||||
|
if (v === null) return 0;
|
||||||
|
return __writeUtf8(v);
|
||||||
|
}
|
||||||
|
function hasAttribute(cookie, namePtr, nameLen) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (!el) return false;
|
||||||
|
return el.hasAttribute(__readUtf8(namePtr, nameLen));
|
||||||
|
}
|
||||||
|
// Checkbox / radio state. el.value on a checkbox returns "on" regardless of
|
||||||
|
// whether it is ticked, so GetValue cannot express this.
|
||||||
|
function getChecked(cookie) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
return !!(el && el.checked);
|
||||||
|
}
|
||||||
|
function setChecked(cookie, checked) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (el) el.checked = !!checked;
|
||||||
|
}
|
||||||
|
// Move keyboard focus. AddFocusListener could observe focus but nothing
|
||||||
|
// could set it, which makes an error summary ("jump to the first invalid
|
||||||
|
// field") impossible to implement accessibly.
|
||||||
|
function focusElement(cookie) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (el && el.focus) el.focus();
|
||||||
|
}
|
||||||
|
function blurElement(cookie) {
|
||||||
|
const el = __jsmemory.get(cookie);
|
||||||
|
if (el && el.blur) el.blur();
|
||||||
|
}
|
||||||
function addClass(cookie, namePtr, nameLen) {
|
function addClass(cookie, namePtr, nameLen) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
if (el) el.classList.add(__readUtf8(namePtr, nameLen));
|
if (el) el.classList.add(__readUtf8(namePtr, nameLen));
|
||||||
|
|
@ -161,12 +211,25 @@ function __dpr() {
|
||||||
return window.crafter_dpr || window.devicePixelRatio || 1;
|
return window.crafter_dpr || window.devicePixelRatio || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `preventDefault` is opt-in per listener (the C++ Add*Listener overloads
|
||||||
|
// take it as a trailing argument). Two reasons it has to exist here rather
|
||||||
|
// than being something C++ can do after the fact:
|
||||||
|
// * the wasm callback runs synchronously inside the dispatch, but the
|
||||||
|
// decision has to be made before the browser acts on the event, and
|
||||||
|
// there is no handle to the event object on the C++ side;
|
||||||
|
// * intercepting a click on a real <a href> — the whole basis of
|
||||||
|
// client-side routing over crawlable links — is impossible without it.
|
||||||
|
// Cancelling happens BEFORE the wasm call so a trap or exception inside the
|
||||||
|
// handler can't leave the default action to fire anyway.
|
||||||
|
// `{ passive: false }` is required or the browser silently ignores
|
||||||
|
// preventDefault on wheel (and warns); harmless on the other kinds.
|
||||||
function __makeMouseListenerPair(kind, eventName, exportName) {
|
function __makeMouseListenerPair(kind, eventName, exportName) {
|
||||||
return {
|
return {
|
||||||
add(cookie, id) {
|
add(cookie, id, preventDefault) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const handler = (event) => {
|
const handler = (event) => {
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
const s = __dpr();
|
const s = __dpr();
|
||||||
__wasm()[exportName](id,
|
__wasm()[exportName](id,
|
||||||
event.clientX * s, event.clientY * s,
|
event.clientX * s, event.clientY * s,
|
||||||
|
|
@ -175,7 +238,7 @@ function __makeMouseListenerPair(kind, eventName, exportName) {
|
||||||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||||
};
|
};
|
||||||
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
||||||
el.addEventListener(eventName, handler);
|
el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined);
|
||||||
},
|
},
|
||||||
remove(cookie, id) {
|
remove(cookie, id) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
|
|
@ -188,17 +251,18 @@ function __makeMouseListenerPair(kind, eventName, exportName) {
|
||||||
}
|
}
|
||||||
function __makeKeyListenerPair(kind, eventName, exportName) {
|
function __makeKeyListenerPair(kind, eventName, exportName) {
|
||||||
return {
|
return {
|
||||||
add(cookie, id) {
|
add(cookie, id, preventDefault) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const handler = (event) => {
|
const handler = (event) => {
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
const keyPtr = __writeUtf8(event.key || "");
|
const keyPtr = __writeUtf8(event.key || "");
|
||||||
__wasm()[exportName](id, keyPtr, event.keyCode,
|
__wasm()[exportName](id, keyPtr, event.keyCode,
|
||||||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||||
__wasm().WasmFree(keyPtr);
|
__wasm().WasmFree(keyPtr);
|
||||||
};
|
};
|
||||||
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
||||||
el.addEventListener(eventName, handler);
|
el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined);
|
||||||
},
|
},
|
||||||
remove(cookie, id) {
|
remove(cookie, id) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
|
|
@ -339,9 +403,10 @@ const __scrollPair = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const __wheelPair = {
|
const __wheelPair = {
|
||||||
add(cookie, id) {
|
add(cookie, id, preventDefault) {
|
||||||
const el = __jsmemory.get(cookie); if (!el) return;
|
const el = __jsmemory.get(cookie); if (!el) return;
|
||||||
const handler = (event) => {
|
const handler = (event) => {
|
||||||
|
if (preventDefault) event.preventDefault();
|
||||||
const s = __dpr();
|
const s = __dpr();
|
||||||
__wasm().ExecuteWheelHandler(id,
|
__wasm().ExecuteWheelHandler(id,
|
||||||
event.deltaX, event.deltaY, event.deltaZ, event.deltaMode,
|
event.deltaX, event.deltaY, event.deltaZ, event.deltaMode,
|
||||||
|
|
@ -350,7 +415,10 @@ const __wheelPair = {
|
||||||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||||
};
|
};
|
||||||
__listenerHandlers.set(`${cookie}-${id}-wheel`, handler);
|
__listenerHandlers.set(`${cookie}-${id}-wheel`, handler);
|
||||||
el.addEventListener("wheel", handler);
|
// passive:false is mandatory here — Chrome treats wheel as passive by
|
||||||
|
// default and would otherwise drop the preventDefault with a console
|
||||||
|
// warning rather than an error.
|
||||||
|
el.addEventListener("wheel", handler, preventDefault ? { passive: false } : undefined);
|
||||||
},
|
},
|
||||||
remove(cookie, id) {
|
remove(cookie, id) {
|
||||||
const el = __jsmemory.get(cookie);
|
const el = __jsmemory.get(cookie);
|
||||||
|
|
@ -619,9 +687,45 @@ function removePopStateListener(id) {
|
||||||
if (h) window.removeEventListener("popstate", h);
|
if (h) window.removeEventListener("popstate", h);
|
||||||
__listenerHandlers.delete(`popstate-${id}`);
|
__listenerHandlers.delete(`popstate-${id}`);
|
||||||
}
|
}
|
||||||
|
// replaceState mirrors pushState but rewrites the current history entry
|
||||||
|
// instead of adding one. Needed for anything that changes the URL without
|
||||||
|
// being a distinct "back" destination — filter/sort state, canonicalising a
|
||||||
|
// sloppy incoming URL, or recording scroll position on the entry you are
|
||||||
|
// about to leave.
|
||||||
|
function replaceState(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.replaceState(parsed, titleStr, urlStr);
|
||||||
|
}
|
||||||
function getPathName() {
|
function getPathName() {
|
||||||
return __writeUtf8(window.location.pathname);
|
return __writeUtf8(window.location.pathname);
|
||||||
}
|
}
|
||||||
|
// location.search / .hash, including their leading "?" / "#" so an empty
|
||||||
|
// query and "?" are distinguishable, and so the result can be concatenated
|
||||||
|
// straight back onto a path. pathname alone left query strings completely
|
||||||
|
// unreachable from wasm, which made ?page= / ?sort= impossible to read.
|
||||||
|
function getSearch() {
|
||||||
|
return __writeUtf8(window.location.search);
|
||||||
|
}
|
||||||
|
function getHash() {
|
||||||
|
return __writeUtf8(window.location.hash);
|
||||||
|
}
|
||||||
|
function getHref() {
|
||||||
|
return __writeUtf8(window.location.href);
|
||||||
|
}
|
||||||
|
// Full-page navigation, including cross-origin. pushState deliberately
|
||||||
|
// cannot leave the origin, so this is the only way for a wasm app to hand
|
||||||
|
// the user off to an external URL (an OAuth consent screen, a hosted
|
||||||
|
// payment page). `replace` omits the current page from history so Back
|
||||||
|
// doesn't return to a stale checkout.
|
||||||
|
function navigate(urlPtr, urlLen, replace) {
|
||||||
|
const url = __readUtf8(urlPtr, urlLen);
|
||||||
|
if (replace) window.location.replace(url);
|
||||||
|
else window.location.assign(url);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Gamepad polling helper ───────────────────────────────────────────
|
// ─── Gamepad polling helper ───────────────────────────────────────────
|
||||||
//
|
//
|
||||||
|
|
@ -659,6 +763,8 @@ Object.assign(window.crafter_webbuild_env, {
|
||||||
// DOM lookup / creation / mutation
|
// DOM lookup / creation / mutation
|
||||||
freeJs, getElementById, createElement, getBody,
|
freeJs, getElementById, createElement, getBody,
|
||||||
setInnerHTML, setStyle, setProperty,
|
setInnerHTML, setStyle, setProperty,
|
||||||
|
setAttribute, removeAttribute, getAttribute, hasAttribute,
|
||||||
|
getChecked, setChecked, focusElement, blurElement,
|
||||||
addClass, removeClass, toggleClass, hasClass,
|
addClass, removeClass, toggleClass, hasClass,
|
||||||
deleteElement, getValue, setValue,
|
deleteElement, getValue, setValue,
|
||||||
|
|
||||||
|
|
@ -697,7 +803,8 @@ Object.assign(window.crafter_webbuild_env, {
|
||||||
clipboardSetText, clipboardGetText,
|
clipboardSetText, clipboardGetText,
|
||||||
|
|
||||||
// History
|
// History
|
||||||
pushState, addPopStateListener, removePopStateListener, getPathName,
|
pushState, replaceState, addPopStateListener, removePopStateListener,
|
||||||
|
getPathName, getSearch, getHash, getHref, navigate,
|
||||||
|
|
||||||
// Gamepad
|
// Gamepad
|
||||||
gamepadPollConnected, gamepadPollDisconnected,
|
gamepadPollConnected, gamepadPollDisconnected,
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,16 @@ document.body.appendChild(canvas);
|
||||||
// when set, the canvas is reparented into that element and sized to it.
|
// when set, the canvas is reparented into that element and sized to it.
|
||||||
let mountEl = null;
|
let mountEl = null;
|
||||||
|
|
||||||
|
// Upper bound on the canvas backing store, in pixels. Infinity until an RT
|
||||||
|
// dispatch derives the real budget from the device's buffer limits (see
|
||||||
|
// wgpuDispatchRT): the wavefront ray buffers scale with W·H·raysPerPixel,
|
||||||
|
// and a 256 MiB maxBufferSize (Firefox's baseline) overflows at ~0.5 Mpx
|
||||||
|
// for 4 rays/pixel — far below a HiDPI 16:9 mount — failing not at the
|
||||||
|
// createBuffer call but as an uncapturable device OOM at submit. The
|
||||||
|
// mounted canvas is CSS-pinned to width:100%/height:100%, so a clamped
|
||||||
|
// backing store upscales on screen instead of shrinking.
|
||||||
|
let maxCanvasPixels = Infinity;
|
||||||
|
|
||||||
function syncCanvasSize() {
|
function syncCanvasSize() {
|
||||||
// Canvas pixel size = CSS size × devicePixelRatio so the GPU draws
|
// Canvas pixel size = CSS size × devicePixelRatio so the GPU draws
|
||||||
// at physical pixel resolution on HiDPI displays — otherwise the
|
// at physical pixel resolution on HiDPI displays — otherwise the
|
||||||
|
|
@ -117,8 +127,13 @@ function syncCanvasSize() {
|
||||||
cssW = window.innerWidth;
|
cssW = window.innerWidth;
|
||||||
cssH = window.innerHeight;
|
cssH = window.innerHeight;
|
||||||
}
|
}
|
||||||
const w = Math.max(1, Math.round(cssW * dpr));
|
let w = Math.max(1, Math.round(cssW * dpr));
|
||||||
const h = Math.max(1, Math.round(cssH * dpr));
|
let h = Math.max(1, Math.round(cssH * dpr));
|
||||||
|
if (w * h > maxCanvasPixels) {
|
||||||
|
const s = Math.sqrt(maxCanvasPixels / (w * h));
|
||||||
|
w = Math.max(1, Math.floor(w * s));
|
||||||
|
h = Math.max(1, Math.floor(h * s));
|
||||||
|
}
|
||||||
if (canvas.width !== w) canvas.width = w;
|
if (canvas.width !== w) canvas.width = w;
|
||||||
if (canvas.height !== h) canvas.height = h;
|
if (canvas.height !== h) canvas.height = h;
|
||||||
return { w, h };
|
return { w, h };
|
||||||
|
|
@ -3511,6 +3526,28 @@ env.wgpuDispatchRT = (pipelineHandle, pushPtr, pushBytes,
|
||||||
}
|
}
|
||||||
const W = state.width, H = state.height;
|
const W = state.width, H = state.height;
|
||||||
const depth = Math.max(1, maxDepth | 0);
|
const depth = Math.max(1, maxDepth | 0);
|
||||||
|
// Clamp the render resolution to what the wavefront buffers can hold.
|
||||||
|
// The payload store is the largest per-ray cost (2 regions ×
|
||||||
|
// WF_PAYLOAD_BYTES) and must fit both maxBufferSize and one storage
|
||||||
|
// binding; the indirect TRACE/SHADE dispatch additionally bounds rays
|
||||||
|
// at maxComputeWorkgroupsPerDimension×64. When the canvas is over
|
||||||
|
// budget, tighten maxCanvasPixels and sit this frame out — the next
|
||||||
|
// wgpuFrameBegin's ensureSized() shrinks the backing store (CSS keeps
|
||||||
|
// the on-screen size) and rendering resumes within budget.
|
||||||
|
const rpp = Math.max(1, raysPerPixel | 0);
|
||||||
|
const rayBudget = Math.min(
|
||||||
|
Math.floor(Math.min(device.limits.maxBufferSize,
|
||||||
|
device.limits.maxStorageBufferBindingSize)
|
||||||
|
/ (2 * WF_PAYLOAD_BYTES)),
|
||||||
|
device.limits.maxComputeWorkgroupsPerDimension * 64);
|
||||||
|
const pixelBudget = Math.max(1, Math.floor(rayBudget / rpp));
|
||||||
|
if (pixelBudget < maxCanvasPixels) maxCanvasPixels = pixelBudget;
|
||||||
|
if (W * H > maxCanvasPixels) {
|
||||||
|
console.warn(`[crafter-wgpu] ${W}x${H} at ${rpp} rays/px overflows the `
|
||||||
|
+ `wavefront budget (maxBufferSize ${device.limits.maxBufferSize}); `
|
||||||
|
+ `reducing render resolution to ~${maxCanvasPixels}px`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const wf = ensureWavefrontBuffers(W, H, raysPerPixel);
|
const wf = ensureWavefrontBuffers(W, H, raysPerPixel);
|
||||||
const cap = wf.cap; // per-bounce ray capacity = raysPerPixel·W·H
|
const cap = wf.cap; // per-bounce ray capacity = raysPerPixel·W·H
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,24 @@ namespace Crafter::DomBindings {
|
||||||
void SetProperty(std::int32_t ptr,
|
void SetProperty(std::int32_t ptr,
|
||||||
const char* property, std::int32_t propertyLength,
|
const char* property, std::int32_t propertyLength,
|
||||||
const char* value, std::int32_t valueLength);
|
const char* value, std::int32_t valueLength);
|
||||||
|
__attribute__((import_module("env"), import_name("setAttribute")))
|
||||||
|
void SetAttribute(std::int32_t ptr,
|
||||||
|
const char* name, std::int32_t nameLength,
|
||||||
|
const char* value, std::int32_t valueLength);
|
||||||
|
__attribute__((import_module("env"), import_name("removeAttribute")))
|
||||||
|
void RemoveAttribute(std::int32_t ptr, const char* name, std::int32_t nameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("getAttribute")))
|
||||||
|
const char* GetAttribute(std::int32_t ptr, const char* name, std::int32_t nameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("hasAttribute")))
|
||||||
|
bool HasAttribute(std::int32_t ptr, const char* name, std::int32_t nameLength);
|
||||||
|
__attribute__((import_module("env"), import_name("getChecked")))
|
||||||
|
bool GetChecked(std::int32_t ptr);
|
||||||
|
__attribute__((import_module("env"), import_name("setChecked")))
|
||||||
|
void SetChecked(std::int32_t ptr, bool checked);
|
||||||
|
__attribute__((import_module("env"), import_name("focusElement")))
|
||||||
|
void FocusElement(std::int32_t ptr);
|
||||||
|
__attribute__((import_module("env"), import_name("blurElement")))
|
||||||
|
void BlurElement(std::int32_t ptr);
|
||||||
__attribute__((import_module("env"), import_name("addClass")))
|
__attribute__((import_module("env"), import_name("addClass")))
|
||||||
void AddClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
void AddClass(std::int32_t ptr, const char* className, std::int32_t classNameLength);
|
||||||
__attribute__((import_module("env"), import_name("removeClass")))
|
__attribute__((import_module("env"), import_name("removeClass")))
|
||||||
|
|
@ -57,37 +75,54 @@ namespace Crafter::DomBindings {
|
||||||
void SetValue(std::int32_t ptr, const char* value, std::int32_t valueLength);
|
void SetValue(std::int32_t ptr, const char* value, std::int32_t valueLength);
|
||||||
|
|
||||||
// Per-event-kind listener register / unregister imports.
|
// Per-event-kind listener register / unregister imports.
|
||||||
|
//
|
||||||
|
// Two flavours, because only some DOM events are cancelable:
|
||||||
|
// CG_DOM_LISTENER_IMPORT_CANCELABLE — mouse, key and wheel kinds. The
|
||||||
|
// add takes a trailing `preventDefault` the JS side applies before
|
||||||
|
// dispatching into wasm (see additional/dom-env.js).
|
||||||
|
// CG_DOM_LISTENER_IMPORT — everything else. focus/blur/change/input/
|
||||||
|
// resize/scroll are not cancelable, so a flag there would be a
|
||||||
|
// silent no-op and is better left off the API. `submit` is the odd
|
||||||
|
// one out: its JS handler ALWAYS calls preventDefault (a native form
|
||||||
|
// POST would navigate away from the wasm app), so it has no flag and
|
||||||
|
// no way to opt out.
|
||||||
#define CG_DOM_LISTENER_IMPORT(addName, removeName) \
|
#define CG_DOM_LISTENER_IMPORT(addName, removeName) \
|
||||||
__attribute__((import_module("env"), import_name(#addName))) \
|
__attribute__((import_module("env"), import_name(#addName))) \
|
||||||
void addName(std::int32_t ptr, std::int32_t id); \
|
void addName(std::int32_t ptr, std::int32_t id); \
|
||||||
__attribute__((import_module("env"), import_name(#removeName))) \
|
__attribute__((import_module("env"), import_name(#removeName))) \
|
||||||
void removeName(std::int32_t ptr, std::int32_t id);
|
void removeName(std::int32_t ptr, std::int32_t id);
|
||||||
|
#define CG_DOM_LISTENER_IMPORT_CANCELABLE(addName, removeName) \
|
||||||
|
__attribute__((import_module("env"), import_name(#addName))) \
|
||||||
|
void addName(std::int32_t ptr, std::int32_t id, bool preventDefault); \
|
||||||
|
__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_CANCELABLE(addClickListener, removeClickListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addMouseOverListener, removeMouseOverListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addMouseOverListener, removeMouseOverListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addMouseOutListener, removeMouseOutListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addMouseOutListener, removeMouseOutListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addMouseMoveListener, removeMouseMoveListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addMouseMoveListener, removeMouseMoveListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addMouseDownListener, removeMouseDownListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addMouseDownListener, removeMouseDownListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addMouseUpListener, removeMouseUpListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addMouseUpListener, removeMouseUpListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addFocusListener, removeFocusListener)
|
CG_DOM_LISTENER_IMPORT(addFocusListener, removeFocusListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addBlurListener, removeBlurListener)
|
CG_DOM_LISTENER_IMPORT(addBlurListener, removeBlurListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addKeyDownListener, removeKeyDownListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addKeyDownListener, removeKeyDownListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addKeyUpListener, removeKeyUpListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addKeyUpListener, removeKeyUpListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addKeyPressListener, removeKeyPressListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addKeyPressListener, removeKeyPressListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addChangeListener, removeChangeListener)
|
CG_DOM_LISTENER_IMPORT(addChangeListener, removeChangeListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addSubmitListener, removeSubmitListener)
|
CG_DOM_LISTENER_IMPORT(addSubmitListener, removeSubmitListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addInputListener, removeInputListener)
|
CG_DOM_LISTENER_IMPORT(addInputListener, removeInputListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addResizeListener, removeResizeListener)
|
CG_DOM_LISTENER_IMPORT(addResizeListener, removeResizeListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addScrollListener, removeScrollListener)
|
CG_DOM_LISTENER_IMPORT(addScrollListener, removeScrollListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addContextMenuListener, removeContextMenuListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addContextMenuListener, removeContextMenuListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDragStartListener, removeDragStartListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDragStartListener, removeDragStartListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDragEndListener, removeDragEndListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDragEndListener, removeDragEndListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDropListener, removeDropListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDropListener, removeDropListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDragOverListener, removeDragOverListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDragOverListener, removeDragOverListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDragEnterListener, removeDragEnterListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDragEnterListener, removeDragEnterListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addDragLeaveListener, removeDragLeaveListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addDragLeaveListener, removeDragLeaveListener)
|
||||||
CG_DOM_LISTENER_IMPORT(addWheelListener, removeWheelListener)
|
CG_DOM_LISTENER_IMPORT_CANCELABLE(addWheelListener, removeWheelListener)
|
||||||
#undef CG_DOM_LISTENER_IMPORT
|
#undef CG_DOM_LISTENER_IMPORT
|
||||||
|
#undef CG_DOM_LISTENER_IMPORT_CANCELABLE
|
||||||
|
|
||||||
// Per-event-kind callback maps. Counters are per-kind so two
|
// Per-event-kind callback maps. Counters are per-kind so two
|
||||||
// different event kinds can share an id without aliasing — the JS
|
// different event kinds can share an id without aliasing — the JS
|
||||||
|
|
@ -272,6 +307,23 @@ namespace {
|
||||||
trackingList.push_back(id);
|
trackingList.push_back(id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
// As AddImpl, for the cancelable kinds whose JS add takes the extra
|
||||||
|
// preventDefault flag. Separate overload rather than a defaulted
|
||||||
|
// parameter so passing the flag to a non-cancelable kind doesn't compile.
|
||||||
|
template <typename EvT, typename JsAdd>
|
||||||
|
std::int32_t AddCancelableImpl(Crafter::DomBindings::HandlerTable<EvT>& table,
|
||||||
|
std::int32_t ptr,
|
||||||
|
std::vector<std::int32_t>& trackingList,
|
||||||
|
JsAdd jsAdd,
|
||||||
|
std::function<void(EvT)> callback,
|
||||||
|
bool preventDefault) {
|
||||||
|
if (ptr == 0) return 0;
|
||||||
|
std::int32_t id = table.maxId++;
|
||||||
|
table.map.insert({id, std::move(callback)});
|
||||||
|
jsAdd(ptr, id, preventDefault);
|
||||||
|
trackingList.push_back(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
template <typename JsAdd>
|
template <typename JsAdd>
|
||||||
std::int32_t AddVoidImpl(Crafter::DomBindings::HandlerTable<void>& table,
|
std::int32_t AddVoidImpl(Crafter::DomBindings::HandlerTable<void>& table,
|
||||||
std::int32_t ptr,
|
std::int32_t ptr,
|
||||||
|
|
@ -412,6 +464,42 @@ namespace Crafter::Dom {
|
||||||
if (ptr) Crafter::DomBindings::SetValue(ptr, value.data(), value.size());
|
if (ptr) Crafter::DomBindings::SetValue(ptr, value.data(), value.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HtmlElementPtr::SetAttribute(const std::string_view name,
|
||||||
|
const std::string_view value) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetAttribute(ptr, name.data(), name.size(),
|
||||||
|
value.data(), value.size());
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::RemoveAttribute(const std::string_view name) {
|
||||||
|
if (ptr) Crafter::DomBindings::RemoveAttribute(ptr, name.data(), name.size());
|
||||||
|
}
|
||||||
|
std::optional<std::string> HtmlElementPtr::GetAttribute(const std::string_view name) {
|
||||||
|
if (!ptr) return std::nullopt;
|
||||||
|
const char* raw = Crafter::DomBindings::GetAttribute(ptr, name.data(), name.size());
|
||||||
|
// 0 means the attribute is absent, which is distinct from present-but-
|
||||||
|
// empty — for boolean attributes presence alone is the signal.
|
||||||
|
if (!raw) return std::nullopt;
|
||||||
|
std::string out(raw);
|
||||||
|
std::free(const_cast<char*>(raw));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
bool HtmlElementPtr::HasAttribute(const std::string_view name) {
|
||||||
|
if (!ptr) return false;
|
||||||
|
return Crafter::DomBindings::HasAttribute(ptr, name.data(), name.size());
|
||||||
|
}
|
||||||
|
bool HtmlElementPtr::GetChecked() {
|
||||||
|
if (!ptr) return false;
|
||||||
|
return Crafter::DomBindings::GetChecked(ptr);
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::SetChecked(bool checked) {
|
||||||
|
if (ptr) Crafter::DomBindings::SetChecked(ptr, checked);
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::Focus() {
|
||||||
|
if (ptr) Crafter::DomBindings::FocusElement(ptr);
|
||||||
|
}
|
||||||
|
void HtmlElementPtr::Blur() {
|
||||||
|
if (ptr) Crafter::DomBindings::BlurElement(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
// Listener wrappers. Each Add/Remove pair just plugs the right
|
// Listener wrappers. Each Add/Remove pair just plugs the right
|
||||||
// table, kind index, and js import into the helper templates. The
|
// table, kind index, and js import into the helper templates. The
|
||||||
// 23 (+1 for popstate, lived in :Router) listener kinds previously
|
// 23 (+1 for popstate, lived in :Router) listener kinds previously
|
||||||
|
|
@ -423,6 +511,13 @@ namespace Crafter::Dom {
|
||||||
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum], \
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum], \
|
||||||
Crafter::DomBindings::JsAdd, std::move(cb)); \
|
Crafter::DomBindings::JsAdd, std::move(cb)); \
|
||||||
}
|
}
|
||||||
|
#define CG_DOM_ADD_CANCELABLE(MethodName, JsAdd, TableName, EvType, KindEnum) \
|
||||||
|
std::int32_t HtmlElementPtr::MethodName(std::function<void(EvType)> cb, \
|
||||||
|
bool preventDefault) { \
|
||||||
|
return AddCancelableImpl<EvType>(Crafter::DomBindings::TableName, ptr, \
|
||||||
|
handlerIds_[(std::size_t)Crafter::DomBindings::Kind::KindEnum], \
|
||||||
|
Crafter::DomBindings::JsAdd, std::move(cb), preventDefault); \
|
||||||
|
}
|
||||||
#define CG_DOM_REMOVE(MethodName, JsRemove, TableName, KindEnum) \
|
#define CG_DOM_REMOVE(MethodName, JsRemove, TableName, KindEnum) \
|
||||||
void HtmlElementPtr::MethodName(std::int32_t id) { \
|
void HtmlElementPtr::MethodName(std::int32_t id) { \
|
||||||
RemoveImpl(Crafter::DomBindings::TableName, ptr, \
|
RemoveImpl(Crafter::DomBindings::TableName, ptr, \
|
||||||
|
|
@ -430,27 +525,27 @@ namespace Crafter::Dom {
|
||||||
Crafter::DomBindings::JsRemove, id); \
|
Crafter::DomBindings::JsRemove, id); \
|
||||||
}
|
}
|
||||||
|
|
||||||
CG_DOM_ADD (AddClickListener, addClickListener, clickT, MouseEvent, Click)
|
CG_DOM_ADD_CANCELABLE(AddClickListener, addClickListener, clickT, MouseEvent, Click)
|
||||||
CG_DOM_REMOVE(RemoveClickListener, removeClickListener, clickT, Click)
|
CG_DOM_REMOVE(RemoveClickListener, removeClickListener, clickT, Click)
|
||||||
CG_DOM_ADD (AddMouseOverListener, addMouseOverListener, mouseOverT, MouseEvent, MouseOver)
|
CG_DOM_ADD_CANCELABLE(AddMouseOverListener, addMouseOverListener, mouseOverT, MouseEvent, MouseOver)
|
||||||
CG_DOM_REMOVE(RemoveMouseOverListener, removeMouseOverListener, mouseOverT, MouseOver)
|
CG_DOM_REMOVE(RemoveMouseOverListener, removeMouseOverListener, mouseOverT, MouseOver)
|
||||||
CG_DOM_ADD (AddMouseOutListener, addMouseOutListener, mouseOutT, MouseEvent, MouseOut)
|
CG_DOM_ADD_CANCELABLE(AddMouseOutListener, addMouseOutListener, mouseOutT, MouseEvent, MouseOut)
|
||||||
CG_DOM_REMOVE(RemoveMouseOutListener, removeMouseOutListener, mouseOutT, MouseOut)
|
CG_DOM_REMOVE(RemoveMouseOutListener, removeMouseOutListener, mouseOutT, MouseOut)
|
||||||
CG_DOM_ADD (AddMouseMoveListener, addMouseMoveListener, mouseMoveT, MouseEvent, MouseMove)
|
CG_DOM_ADD_CANCELABLE(AddMouseMoveListener, addMouseMoveListener, mouseMoveT, MouseEvent, MouseMove)
|
||||||
CG_DOM_REMOVE(RemoveMouseMoveListener, removeMouseMoveListener, mouseMoveT, MouseMove)
|
CG_DOM_REMOVE(RemoveMouseMoveListener, removeMouseMoveListener, mouseMoveT, MouseMove)
|
||||||
CG_DOM_ADD (AddMouseDownListener, addMouseDownListener, mouseDownT, MouseEvent, MouseDown)
|
CG_DOM_ADD_CANCELABLE(AddMouseDownListener, addMouseDownListener, mouseDownT, MouseEvent, MouseDown)
|
||||||
CG_DOM_REMOVE(RemoveMouseDownListener, removeMouseDownListener, mouseDownT, MouseDown)
|
CG_DOM_REMOVE(RemoveMouseDownListener, removeMouseDownListener, mouseDownT, MouseDown)
|
||||||
CG_DOM_ADD (AddMouseUpListener, addMouseUpListener, mouseUpT, MouseEvent, MouseUp)
|
CG_DOM_ADD_CANCELABLE(AddMouseUpListener, addMouseUpListener, mouseUpT, MouseEvent, MouseUp)
|
||||||
CG_DOM_REMOVE(RemoveMouseUpListener, removeMouseUpListener, mouseUpT, MouseUp)
|
CG_DOM_REMOVE(RemoveMouseUpListener, removeMouseUpListener, mouseUpT, MouseUp)
|
||||||
CG_DOM_ADD (AddFocusListener, addFocusListener, focusT, FocusEvent, Focus)
|
CG_DOM_ADD (AddFocusListener, addFocusListener, focusT, FocusEvent, Focus)
|
||||||
CG_DOM_REMOVE(RemoveFocusListener, removeFocusListener, focusT, Focus)
|
CG_DOM_REMOVE(RemoveFocusListener, removeFocusListener, focusT, Focus)
|
||||||
CG_DOM_ADD (AddBlurListener, addBlurListener, blurT, FocusEvent, Blur)
|
CG_DOM_ADD (AddBlurListener, addBlurListener, blurT, FocusEvent, Blur)
|
||||||
CG_DOM_REMOVE(RemoveBlurListener, removeBlurListener, blurT, Blur)
|
CG_DOM_REMOVE(RemoveBlurListener, removeBlurListener, blurT, Blur)
|
||||||
CG_DOM_ADD (AddKeyDownListener, addKeyDownListener, keyDownT, KeyboardEvent, KeyDown)
|
CG_DOM_ADD_CANCELABLE(AddKeyDownListener, addKeyDownListener, keyDownT, KeyboardEvent, KeyDown)
|
||||||
CG_DOM_REMOVE(RemoveKeyDownListener, removeKeyDownListener, keyDownT, KeyDown)
|
CG_DOM_REMOVE(RemoveKeyDownListener, removeKeyDownListener, keyDownT, KeyDown)
|
||||||
CG_DOM_ADD (AddKeyUpListener, addKeyUpListener, keyUpT, KeyboardEvent, KeyUp)
|
CG_DOM_ADD_CANCELABLE(AddKeyUpListener, addKeyUpListener, keyUpT, KeyboardEvent, KeyUp)
|
||||||
CG_DOM_REMOVE(RemoveKeyUpListener, removeKeyUpListener, keyUpT, KeyUp)
|
CG_DOM_REMOVE(RemoveKeyUpListener, removeKeyUpListener, keyUpT, KeyUp)
|
||||||
CG_DOM_ADD (AddKeyPressListener, addKeyPressListener, keyPressT, KeyboardEvent, KeyPress)
|
CG_DOM_ADD_CANCELABLE(AddKeyPressListener, addKeyPressListener, keyPressT, KeyboardEvent, KeyPress)
|
||||||
CG_DOM_REMOVE(RemoveKeyPressListener, removeKeyPressListener, keyPressT, KeyPress)
|
CG_DOM_REMOVE(RemoveKeyPressListener, removeKeyPressListener, keyPressT, KeyPress)
|
||||||
CG_DOM_ADD (AddChangeListener, addChangeListener, changeT, ChangeEvent, Change)
|
CG_DOM_ADD (AddChangeListener, addChangeListener, changeT, ChangeEvent, Change)
|
||||||
CG_DOM_REMOVE(RemoveChangeListener, removeChangeListener, changeT, Change)
|
CG_DOM_REMOVE(RemoveChangeListener, removeChangeListener, changeT, Change)
|
||||||
|
|
@ -460,21 +555,21 @@ namespace Crafter::Dom {
|
||||||
CG_DOM_REMOVE(RemoveResizeListener, removeResizeListener, resizeT, Resize)
|
CG_DOM_REMOVE(RemoveResizeListener, removeResizeListener, resizeT, Resize)
|
||||||
CG_DOM_ADD (AddScrollListener, addScrollListener, scrollT, ScrollEvent, Scroll)
|
CG_DOM_ADD (AddScrollListener, addScrollListener, scrollT, ScrollEvent, Scroll)
|
||||||
CG_DOM_REMOVE(RemoveScrollListener, removeScrollListener, scrollT, Scroll)
|
CG_DOM_REMOVE(RemoveScrollListener, removeScrollListener, scrollT, Scroll)
|
||||||
CG_DOM_ADD (AddContextMenuListener, addContextMenuListener, contextMenuT, MouseEvent, ContextMenu)
|
CG_DOM_ADD_CANCELABLE(AddContextMenuListener, addContextMenuListener, contextMenuT, MouseEvent, ContextMenu)
|
||||||
CG_DOM_REMOVE(RemoveContextMenuListener,removeContextMenuListener,contextMenuT, ContextMenu)
|
CG_DOM_REMOVE(RemoveContextMenuListener,removeContextMenuListener,contextMenuT, ContextMenu)
|
||||||
CG_DOM_ADD (AddDragStartListener, addDragStartListener, dragStartT, MouseEvent, DragStart)
|
CG_DOM_ADD_CANCELABLE(AddDragStartListener, addDragStartListener, dragStartT, MouseEvent, DragStart)
|
||||||
CG_DOM_REMOVE(RemoveDragStartListener, removeDragStartListener, dragStartT, DragStart)
|
CG_DOM_REMOVE(RemoveDragStartListener, removeDragStartListener, dragStartT, DragStart)
|
||||||
CG_DOM_ADD (AddDragEndListener, addDragEndListener, dragEndT, MouseEvent, DragEnd)
|
CG_DOM_ADD_CANCELABLE(AddDragEndListener, addDragEndListener, dragEndT, MouseEvent, DragEnd)
|
||||||
CG_DOM_REMOVE(RemoveDragEndListener, removeDragEndListener, dragEndT, DragEnd)
|
CG_DOM_REMOVE(RemoveDragEndListener, removeDragEndListener, dragEndT, DragEnd)
|
||||||
CG_DOM_ADD (AddDropListener, addDropListener, dropT, MouseEvent, Drop)
|
CG_DOM_ADD_CANCELABLE(AddDropListener, addDropListener, dropT, MouseEvent, Drop)
|
||||||
CG_DOM_REMOVE(RemoveDropListener, removeDropListener, dropT, Drop)
|
CG_DOM_REMOVE(RemoveDropListener, removeDropListener, dropT, Drop)
|
||||||
CG_DOM_ADD (AddDragOverListener, addDragOverListener, dragOverT, MouseEvent, DragOver)
|
CG_DOM_ADD_CANCELABLE(AddDragOverListener, addDragOverListener, dragOverT, MouseEvent, DragOver)
|
||||||
CG_DOM_REMOVE(RemoveDragOverListener, removeDragOverListener, dragOverT, DragOver)
|
CG_DOM_REMOVE(RemoveDragOverListener, removeDragOverListener, dragOverT, DragOver)
|
||||||
CG_DOM_ADD (AddDragEnterListener, addDragEnterListener, dragEnterT, MouseEvent, DragEnter)
|
CG_DOM_ADD_CANCELABLE(AddDragEnterListener, addDragEnterListener, dragEnterT, MouseEvent, DragEnter)
|
||||||
CG_DOM_REMOVE(RemoveDragEnterListener, removeDragEnterListener, dragEnterT, DragEnter)
|
CG_DOM_REMOVE(RemoveDragEnterListener, removeDragEnterListener, dragEnterT, DragEnter)
|
||||||
CG_DOM_ADD (AddDragLeaveListener, addDragLeaveListener, dragLeaveT, MouseEvent, DragLeave)
|
CG_DOM_ADD_CANCELABLE(AddDragLeaveListener, addDragLeaveListener, dragLeaveT, MouseEvent, DragLeave)
|
||||||
CG_DOM_REMOVE(RemoveDragLeaveListener, removeDragLeaveListener, dragLeaveT, DragLeave)
|
CG_DOM_REMOVE(RemoveDragLeaveListener, removeDragLeaveListener, dragLeaveT, DragLeave)
|
||||||
CG_DOM_ADD (AddWheelListener, addWheelListener, wheelT, WheelEvent, Wheel)
|
CG_DOM_ADD_CANCELABLE(AddWheelListener, addWheelListener, wheelT, WheelEvent, Wheel)
|
||||||
CG_DOM_REMOVE(RemoveWheelListener, removeWheelListener, wheelT, Wheel)
|
CG_DOM_REMOVE(RemoveWheelListener, removeWheelListener, wheelT, Wheel)
|
||||||
#undef CG_DOM_ADD
|
#undef CG_DOM_ADD
|
||||||
#undef CG_DOM_REMOVE
|
#undef CG_DOM_REMOVE
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,20 @@ namespace Crafter::DomBindings {
|
||||||
void AddPopStateListener(std::int32_t id);
|
void AddPopStateListener(std::int32_t id);
|
||||||
__attribute__((import_module("env"), import_name("removePopStateListener")))
|
__attribute__((import_module("env"), import_name("removePopStateListener")))
|
||||||
void RemovePopStateListener(std::int32_t id);
|
void RemovePopStateListener(std::int32_t id);
|
||||||
|
__attribute__((import_module("env"), import_name("replaceState")))
|
||||||
|
void ReplaceState(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("getPathName")))
|
__attribute__((import_module("env"), import_name("getPathName")))
|
||||||
const char* GetPathName();
|
const char* GetPathName();
|
||||||
|
__attribute__((import_module("env"), import_name("getSearch")))
|
||||||
|
const char* GetSearch();
|
||||||
|
__attribute__((import_module("env"), import_name("getHash")))
|
||||||
|
const char* GetHash();
|
||||||
|
__attribute__((import_module("env"), import_name("getHref")))
|
||||||
|
const char* GetHref();
|
||||||
|
__attribute__((import_module("env"), import_name("navigate")))
|
||||||
|
void Navigate(const char* url, std::int32_t urlLength, bool replace);
|
||||||
|
|
||||||
// Defined in Crafter.Graphics-Dom.cpp.
|
// Defined in Crafter.Graphics-Dom.cpp.
|
||||||
std::int32_t PopStateRegister(std::function<void()> cb);
|
std::int32_t PopStateRegister(std::function<void()> cb);
|
||||||
|
|
@ -48,11 +60,114 @@ namespace Crafter::Router {
|
||||||
Crafter::DomBindings::PopStateUnregister(id);
|
Crafter::DomBindings::PopStateUnregister(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetPath() {
|
void ReplaceState(std::string_view data, std::string_view title, std::string_view url) {
|
||||||
const char* raw = Crafter::DomBindings::GetPathName();
|
Crafter::DomBindings::ReplaceState(
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The four location accessors share the same shape: the JS side
|
||||||
|
// WasmAllocs a NUL-terminated UTF-8 copy, we adopt it into a
|
||||||
|
// std::string and free the raw buffer. A null return means the JS
|
||||||
|
// allocation failed.
|
||||||
|
namespace {
|
||||||
|
std::string AdoptJsString(const char* raw) {
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
std::string out(raw);
|
std::string out(raw);
|
||||||
std::free(const_cast<char*>(raw));
|
std::free(const_cast<char*>(raw));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string GetPath() {
|
||||||
|
return AdoptJsString(Crafter::DomBindings::GetPathName());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetSearch() {
|
||||||
|
return AdoptJsString(Crafter::DomBindings::GetSearch());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetHash() {
|
||||||
|
return AdoptJsString(Crafter::DomBindings::GetHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetHref() {
|
||||||
|
return AdoptJsString(Crafter::DomBindings::GetHref());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Navigate(std::string_view url, bool replace) {
|
||||||
|
Crafter::DomBindings::Navigate(
|
||||||
|
url.data(), static_cast<std::int32_t>(url.size()), replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Query-string parsing ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Pure string work, no JS round-trip. Kept here rather than left to
|
||||||
|
// callers because every consumer would otherwise hand-roll percent
|
||||||
|
// decoding, and getting `+` vs `%20` wrong is the usual bug.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int HexVal(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PercentDecode(std::string_view in) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(in.size());
|
||||||
|
for (std::size_t i = 0; i < in.size(); ++i) {
|
||||||
|
const char c = in[i];
|
||||||
|
if (c == '+') {
|
||||||
|
// application/x-www-form-urlencoded encodes space as '+'.
|
||||||
|
out.push_back(' ');
|
||||||
|
} else if (c == '%' && i + 2 < in.size()) {
|
||||||
|
const int hi = HexVal(in[i + 1]);
|
||||||
|
const int lo = HexVal(in[i + 2]);
|
||||||
|
if (hi >= 0 && lo >= 0) {
|
||||||
|
out.push_back(static_cast<char>(hi * 16 + lo));
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
// Malformed escape: pass the '%' through rather than
|
||||||
|
// dropping input. Lets a stray '%' in a search box
|
||||||
|
// survive a round trip.
|
||||||
|
out.push_back(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> ParseQuery(std::string_view query) {
|
||||||
|
std::vector<std::pair<std::string, std::string>> out;
|
||||||
|
if (!query.empty() && query.front() == '?') query.remove_prefix(1);
|
||||||
|
while (!query.empty()) {
|
||||||
|
const std::size_t amp = query.find('&');
|
||||||
|
std::string_view pair = query.substr(0, amp);
|
||||||
|
query = (amp == std::string_view::npos) ? std::string_view{}
|
||||||
|
: query.substr(amp + 1);
|
||||||
|
if (pair.empty()) continue; // tolerate "a=1&&b=2" and a trailing '&'
|
||||||
|
const std::size_t eq = pair.find('=');
|
||||||
|
if (eq == std::string_view::npos) {
|
||||||
|
// Valueless key ("?debug") — present, empty value.
|
||||||
|
out.emplace_back(PercentDecode(pair), std::string{});
|
||||||
|
} else {
|
||||||
|
out.emplace_back(PercentDecode(pair.substr(0, eq)),
|
||||||
|
PercentDecode(pair.substr(eq + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> QueryGet(std::string_view query, std::string_view key) {
|
||||||
|
for (auto& [k, v] : ParseQuery(query)) {
|
||||||
|
if (k == key) return v;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -715,6 +715,17 @@ void Window::StartSync() {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Window::StayAlive() {
|
||||||
|
// Native has no equivalent of the browser's "return from main but keep
|
||||||
|
// the instance alive" trick — the process simply ends. The event loop
|
||||||
|
// IS what keeps a native window alive, so the only correct native
|
||||||
|
// behaviour is StartSync's loop. Delegating keeps a single source that
|
||||||
|
// compiles identically on both targets: an app can call StayAlive
|
||||||
|
// unconditionally and get the cheap path in the browser without a
|
||||||
|
// #ifdef at the call site.
|
||||||
|
StartSync();
|
||||||
|
}
|
||||||
|
|
||||||
void Window::StartUpdate() {
|
void Window::StartUpdate() {
|
||||||
lastFrameBegin = std::chrono::high_resolution_clock::now();
|
lastFrameBegin = std::chrono::high_resolution_clock::now();
|
||||||
updating = true;
|
updating = true;
|
||||||
|
|
@ -1571,6 +1582,23 @@ void Window::StartSync() {
|
||||||
std::_Exit(0);
|
std::_Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Window::StayAlive() {
|
||||||
|
// As StartSync, minus the rAF loop.
|
||||||
|
//
|
||||||
|
// The _Exit(0) is the part that keeps the instance alive: it skips
|
||||||
|
// __wasm_call_dtors so every statically allocated object (event
|
||||||
|
// listeners, HtmlElementPtr handles, the Window itself) survives, and
|
||||||
|
// runtime.js catches the resulting __wasi_proc_exit via a sentinel.
|
||||||
|
// The frame loop is a separate concern, and a page that only renders
|
||||||
|
// DOM has no use for it — a rAF tick that runs forever to do nothing
|
||||||
|
// costs battery on every open tab.
|
||||||
|
//
|
||||||
|
// Use this for event-driven DOM pages; use StartSync when something
|
||||||
|
// actually animates or draws. As with StartSync, no caller code after
|
||||||
|
// this point ever runs.
|
||||||
|
std::_Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
void Window::StartUpdate() {
|
void Window::StartUpdate() {
|
||||||
lastFrameBegin = std::chrono::high_resolution_clock::now();
|
lastFrameBegin = std::chrono::high_resolution_clock::now();
|
||||||
updating = true;
|
updating = true;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,12 @@ export namespace Crafter::Dom {
|
||||||
// DOM ops ─────────────────────────────────────────────────────
|
// DOM ops ─────────────────────────────────────────────────────
|
||||||
void SetInnerHTML(const std::string_view html);
|
void SetInnerHTML(const std::string_view html);
|
||||||
void SetStyle(const std::string_view style);
|
void SetStyle(const std::string_view style);
|
||||||
|
|
||||||
|
// WARNING: SetProperty sets a *CSS* property (el.style.setProperty).
|
||||||
|
// It is NOT setAttribute — the name is historical. For href / src /
|
||||||
|
// disabled / aria-* / data-*, use SetAttribute below.
|
||||||
void SetProperty(const std::string_view property, const std::string_view value);
|
void SetProperty(const std::string_view property, const std::string_view value);
|
||||||
|
|
||||||
void AddClass(const std::string_view className);
|
void AddClass(const std::string_view className);
|
||||||
void RemoveClass(const std::string_view className);
|
void RemoveClass(const std::string_view className);
|
||||||
void ToggleClass(const std::string_view className);
|
void ToggleClass(const std::string_view className);
|
||||||
|
|
@ -69,28 +74,80 @@ export namespace Crafter::Dom {
|
||||||
std::string GetValue();
|
std::string GetValue();
|
||||||
void SetValue(const std::string_view value);
|
void SetValue(const std::string_view value);
|
||||||
|
|
||||||
|
// Attributes ──────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Real setAttribute/getAttribute access. Before these existed the
|
||||||
|
// only way to change an href or toggle `disabled` was to re-render
|
||||||
|
// the parent's innerHTML, which destroys every descendant and every
|
||||||
|
// listener attached to them just to change one string.
|
||||||
|
void SetAttribute(const std::string_view name, const std::string_view value);
|
||||||
|
void RemoveAttribute(const std::string_view name);
|
||||||
|
|
||||||
|
// nullopt = attribute absent. Distinct from an empty string, which
|
||||||
|
// means present-with-no-value (`alt=""`, `disabled=""`) — for
|
||||||
|
// boolean attributes presence alone is the signal.
|
||||||
|
std::optional<std::string> GetAttribute(const std::string_view name);
|
||||||
|
bool HasAttribute(const std::string_view name);
|
||||||
|
|
||||||
|
// Checkbox / radio state. GetValue cannot express this: `el.value`
|
||||||
|
// on a checkbox is "on" whether or not it is ticked.
|
||||||
|
bool GetChecked();
|
||||||
|
void SetChecked(bool checked);
|
||||||
|
|
||||||
|
// Move keyboard focus to / away from this element. AddFocusListener
|
||||||
|
// could already observe focus, but nothing could set it, which makes
|
||||||
|
// an accessible "jump to first invalid field" impossible.
|
||||||
|
void Focus();
|
||||||
|
void Blur();
|
||||||
|
|
||||||
// Listener API — each Add* returns an opaque id that can be
|
// Listener API — each Add* returns an opaque id that can be
|
||||||
// passed to the matching Remove*. The destructor automatically
|
// passed to the matching Remove*. The destructor automatically
|
||||||
// removes every handler still registered, so manual removal is
|
// removes every handler still registered, so manual removal is
|
||||||
// optional. Returns 0 only if registration failed at the JS
|
// optional. Returns 0 only if registration failed at the JS
|
||||||
// boundary (the element was already collected). The 23 event
|
// boundary (the element was already collected). The 23 event
|
||||||
// types are 1:1 with CppDOM's surface.
|
// types are 1:1 with CppDOM's surface.
|
||||||
std::int32_t AddClickListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
//
|
||||||
|
// `preventDefault` (mouse / key / wheel kinds only — the others are
|
||||||
|
// not cancelable events, so the flag would be a silent no-op and is
|
||||||
|
// deliberately absent):
|
||||||
|
// Cancels the browser's default action. The JS bridge applies it
|
||||||
|
// BEFORE dispatching into wasm, so a trap in the callback cannot let
|
||||||
|
// the default action through anyway.
|
||||||
|
//
|
||||||
|
// This is what makes client-side routing over real, crawlable
|
||||||
|
// `<a href="/foo">` links possible: take the click, cancel the
|
||||||
|
// navigation, then Router::PushState. Without it the handler runs
|
||||||
|
// *and* the browser performs a full page load. It is also mandatory
|
||||||
|
// on dragover/drop — a drop target does not function at all unless
|
||||||
|
// the default is cancelled.
|
||||||
|
//
|
||||||
|
// Wheel additionally registers with `{ passive: false }` when set,
|
||||||
|
// or the browser drops the cancellation with a console warning.
|
||||||
|
//
|
||||||
|
// AddSubmitListener has no flag: its handler always preventDefaults,
|
||||||
|
// because a native form POST would navigate away from the wasm app.
|
||||||
|
std::int32_t AddClickListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveClickListener(std::int32_t id);
|
void RemoveClickListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddMouseOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddMouseOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveMouseOverListener(std::int32_t id);
|
void RemoveMouseOverListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddMouseOutListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddMouseOutListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveMouseOutListener(std::int32_t id);
|
void RemoveMouseOutListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddMouseMoveListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddMouseMoveListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveMouseMoveListener(std::int32_t id);
|
void RemoveMouseMoveListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddMouseDownListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddMouseDownListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveMouseDownListener(std::int32_t id);
|
void RemoveMouseDownListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddMouseUpListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddMouseUpListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveMouseUpListener(std::int32_t id);
|
void RemoveMouseUpListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddFocusListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
std::int32_t AddFocusListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
||||||
|
|
@ -99,13 +156,16 @@ export namespace Crafter::Dom {
|
||||||
std::int32_t AddBlurListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
std::int32_t AddBlurListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
|
||||||
void RemoveBlurListener(std::int32_t id);
|
void RemoveBlurListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddKeyDownListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
std::int32_t AddKeyDownListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveKeyDownListener(std::int32_t id);
|
void RemoveKeyDownListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddKeyUpListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
std::int32_t AddKeyUpListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveKeyUpListener(std::int32_t id);
|
void RemoveKeyUpListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddKeyPressListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback);
|
std::int32_t AddKeyPressListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveKeyPressListener(std::int32_t id);
|
void RemoveKeyPressListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddChangeListener(std::function<void(Crafter::Dom::ChangeEvent)> callback);
|
std::int32_t AddChangeListener(std::function<void(Crafter::Dom::ChangeEvent)> callback);
|
||||||
|
|
@ -123,28 +183,36 @@ export namespace Crafter::Dom {
|
||||||
std::int32_t AddScrollListener(std::function<void(Crafter::Dom::ScrollEvent)> callback);
|
std::int32_t AddScrollListener(std::function<void(Crafter::Dom::ScrollEvent)> callback);
|
||||||
void RemoveScrollListener(std::int32_t id);
|
void RemoveScrollListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddContextMenuListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddContextMenuListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveContextMenuListener(std::int32_t id);
|
void RemoveContextMenuListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDragStartListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDragStartListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDragStartListener(std::int32_t id);
|
void RemoveDragStartListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDragEndListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDragEndListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDragEndListener(std::int32_t id);
|
void RemoveDragEndListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDropListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDropListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDropListener(std::int32_t id);
|
void RemoveDropListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDragOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDragOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDragOverListener(std::int32_t id);
|
void RemoveDragOverListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDragEnterListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDragEnterListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDragEnterListener(std::int32_t id);
|
void RemoveDragEnterListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddDragLeaveListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
|
std::int32_t AddDragLeaveListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveDragLeaveListener(std::int32_t id);
|
void RemoveDragLeaveListener(std::int32_t id);
|
||||||
|
|
||||||
std::int32_t AddWheelListener(std::function<void(Crafter::Dom::WheelEvent)> callback);
|
std::int32_t AddWheelListener(std::function<void(Crafter::Dom::WheelEvent)> callback,
|
||||||
|
bool preventDefault = false);
|
||||||
void RemoveWheelListener(std::int32_t id);
|
void RemoveWheelListener(std::int32_t id);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,62 @@ export namespace Crafter::Router {
|
||||||
// Push a new history entry. `data` is a JSON string serialized by
|
// Push a new history entry. `data` is a JSON string serialized by
|
||||||
// the caller — the browser stores it on the entry but the popstate
|
// the caller — the browser stores it on the entry but the popstate
|
||||||
// listener in V1 receives no payload (matches CppDOM's surface).
|
// listener in V1 receives no payload (matches CppDOM's surface).
|
||||||
// `url` is browser-relative, e.g. "/blog/post-1".
|
// `url` is browser-relative, e.g. "/blog/post-1". Cannot leave the
|
||||||
|
// origin; use Navigate for that.
|
||||||
void PushState(std::string_view data, std::string_view title, std::string_view url);
|
void PushState(std::string_view data, std::string_view title, std::string_view url);
|
||||||
|
|
||||||
|
// As PushState, but rewrites the current entry instead of adding one.
|
||||||
|
// For URL changes that shouldn't become their own Back destination:
|
||||||
|
// filter/sort state, canonicalising a sloppy incoming URL.
|
||||||
|
void ReplaceState(std::string_view data, std::string_view title, std::string_view url);
|
||||||
|
|
||||||
// Subscribe to the browser's `popstate` event (back/forward button,
|
// Subscribe to the browser's `popstate` event (back/forward button,
|
||||||
// programmatic history.go). Returns an opaque id usable with
|
// programmatic history.go). Returns an opaque id usable with
|
||||||
// `RemovePopStateListener`. Multiple subscribers OK.
|
// `RemovePopStateListener`. Multiple subscribers OK.
|
||||||
|
//
|
||||||
|
// The callback still receives no payload — re-read the location with
|
||||||
|
// the accessors below rather than relying on the pushState `data`.
|
||||||
std::int32_t AddPopStateListener(std::function<void()> callback);
|
std::int32_t AddPopStateListener(std::function<void()> callback);
|
||||||
void RemovePopStateListener(std::int32_t id);
|
void RemovePopStateListener(std::int32_t id);
|
||||||
|
|
||||||
// Current `window.location.pathname` as a freshly-allocated string.
|
// Location accessors. Each allocates a fresh string per call — cache
|
||||||
// Allocates per call — cache the result in the caller if used in
|
// the result if used in a hot path.
|
||||||
// hot paths.
|
//
|
||||||
std::string GetPath();
|
// GetSearch / GetHash include their leading '?' / '#', so an absent
|
||||||
|
// query and a bare "?" stay distinguishable and the result can be
|
||||||
|
// concatenated straight back onto a path.
|
||||||
|
std::string GetPath(); // window.location.pathname
|
||||||
|
std::string GetSearch(); // window.location.search, e.g. "?page=2&sort=new"
|
||||||
|
std::string GetHash(); // window.location.hash, e.g. "#reviews"
|
||||||
|
std::string GetHref(); // window.location.href, absolute
|
||||||
|
|
||||||
|
// Full-page navigation, cross-origin allowed. PushState deliberately
|
||||||
|
// cannot leave the origin, so this is the only way to hand the user off
|
||||||
|
// to an external URL (a hosted payment page, an OAuth consent screen).
|
||||||
|
// `replace == true` drops the current page from history, so Back won't
|
||||||
|
// return to a stale page.
|
||||||
|
void Navigate(std::string_view url, bool replace = false);
|
||||||
|
|
||||||
|
// ─── Query-string helpers ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Pure string functions — no JS round-trip, safe to call on any
|
||||||
|
// string, not just the live location. Provided here so callers don't
|
||||||
|
// each hand-roll percent decoding; `+` vs `%20` is the usual bug.
|
||||||
|
|
||||||
|
// Split a query string into decoded key/value pairs, in source order.
|
||||||
|
// Accepts an optional leading '?'. Duplicate keys are preserved as
|
||||||
|
// separate entries (?tag=a&tag=b yields two). A valueless key
|
||||||
|
// ("?debug") yields an empty value. Empty segments are skipped, so
|
||||||
|
// "a=1&&b=2" and a trailing '&' are tolerated.
|
||||||
|
std::vector<std::pair<std::string, std::string>> ParseQuery(std::string_view query);
|
||||||
|
|
||||||
|
// First value for `key`, or nullopt if absent. Distinguishes "missing"
|
||||||
|
// from "present but empty" — which ?debug and ?q= need.
|
||||||
|
std::optional<std::string> QueryGet(std::string_view query, std::string_view key);
|
||||||
|
|
||||||
|
// Percent-decode a single component, treating '+' as a space per
|
||||||
|
// application/x-www-form-urlencoded. A malformed escape is passed
|
||||||
|
// through literally rather than dropped.
|
||||||
|
std::string PercentDecode(std::string_view in);
|
||||||
}
|
}
|
||||||
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,22 @@ export namespace Crafter {
|
||||||
~Window();
|
~Window();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Enter the platform event loop and never return. In DOM mode this
|
||||||
|
// hands the loop to requestAnimationFrame and _Exit(0)s so the wasm
|
||||||
|
// instance survives without running static destructors. No caller
|
||||||
|
// code after this point ever runs, on any target.
|
||||||
void StartSync();
|
void StartSync();
|
||||||
|
|
||||||
|
// As StartSync, but does NOT start the animation-frame loop in DOM
|
||||||
|
// mode — for event-driven pages that only touch the DOM and never
|
||||||
|
// draw. A rAF tick running forever to do nothing costs battery in
|
||||||
|
// every open tab. Still never returns.
|
||||||
|
//
|
||||||
|
// On native this is identical to StartSync: the event loop is what
|
||||||
|
// keeps a window alive there, so there is no cheaper option. Call it
|
||||||
|
// unconditionally and get the browser saving without an #ifdef.
|
||||||
|
void StayAlive();
|
||||||
|
|
||||||
void StartUpdate();
|
void StartUpdate();
|
||||||
void StopUpdate();
|
void StopUpdate();
|
||||||
void SetTitle(const std::string_view title);
|
void SetTitle(const std::string_view title);
|
||||||
|
|
|
||||||
24
project.cpp
24
project.cpp
|
|
@ -192,7 +192,31 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// JS glue shipped alongside the .wasm so the loader has the
|
// JS glue shipped alongside the .wasm so the loader has the
|
||||||
// env-import surface the Window/Dom bindings expect.
|
// env-import surface the Window/Dom bindings expect.
|
||||||
cfg.files.emplace_back(fs::path("additional/dom-env.js"));
|
cfg.files.emplace_back(fs::path("additional/dom-env.js"));
|
||||||
|
|
||||||
|
// dom-webgpu.js is opt-out via --no-webgpu, for apps that only use
|
||||||
|
// the Dom/Router partitions and never touch Device/RTPass/UIRenderer.
|
||||||
|
//
|
||||||
|
// Three reasons a DOM-only app wants it gone, in increasing order of
|
||||||
|
// severity:
|
||||||
|
// 1. It is ~182 KB (47 KB gzip) of dead weight.
|
||||||
|
// 2. Its init is an async IIFE that runtime.js awaits before
|
||||||
|
// _start(), and that init does `await navigator.gpu
|
||||||
|
// .requestAdapter()` — so every page load blocks wasm startup on
|
||||||
|
// a GPU adapter request, including on machines with no GPU.
|
||||||
|
// 3. Worst: when navigator.gpu is absent it REPLACES
|
||||||
|
// document.body.innerHTML with an error message and throws, so
|
||||||
|
// _start() never runs. For a server-rendered page that means the
|
||||||
|
// real content is destroyed and replaced with a WebGPU warning on
|
||||||
|
// any browser without WebGPU support.
|
||||||
|
//
|
||||||
|
// Safety: with --gc-sections an app that never calls into the WebGPU
|
||||||
|
// partitions emits no wgpu* env imports, so omitting the bridge links
|
||||||
|
// and instantiates cleanly. An app that DOES use WebGPU and passes
|
||||||
|
// --no-webgpu anyway fails at instantiate with a missing-import error
|
||||||
|
// naming the specific wgpu* symbol.
|
||||||
|
if (!opts.Has("--no-webgpu")) {
|
||||||
cfg.files.emplace_back(fs::path("additional/dom-webgpu.js"));
|
cfg.files.emplace_back(fs::path("additional/dom-webgpu.js"));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
std::array<fs::path, 14> impls = {
|
std::array<fs::path, 14> impls = {
|
||||||
"implementations/Crafter.Graphics-Clipboard",
|
"implementations/Crafter.Graphics-Clipboard",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue