Crafter.Graphics/additional/dom-env.js
2026-05-19 00:45:22 +02:00

598 lines
26 KiB
JavaScript

/*
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 ────────────────────────────────────────────────────────
//
// Read path is necessarily a latched buffer: navigator.clipboard.readText
// is async (Promise) but the C++ Clipboard::GetText signature is
// synchronous to match the Wayland / Win32 backends. Two feeders keep
// the cache fresh:
//
// 1. A capture-phase `paste` listener on `window` reads
// event.clipboardData.getData('text/plain') synchronously — this
// covers the standard Ctrl+V flow, where the paste event fires
// in the same task as the C++ key handler that will call GetText.
// 2. Each GetText call also kicks off a navigator.clipboard.readText
// in the background. The promise resolves in a later task; the
// first call usually still returns the latched paste value (or
// nullopt), subsequent calls see the readText result.
let __clipboardCache = null;
window.addEventListener("paste", (event) => {
try {
const t = event.clipboardData &&
event.clipboardData.getData("text/plain");
if (typeof t === "string") __clipboardCache = t;
} catch (err) {
// Some browsers reject clipboardData access inside non-input
// targets; the async readText fallback still has a chance.
}
}, true);
function clipboardSetText(strPtr, strLen) {
try {
navigator.clipboard.writeText(__readUtf8(strPtr, strLen));
return true;
} catch (err) {
console.error("Crafter.Clipboard.SetText failed:", err);
return false;
}
}
function clipboardGetText() {
// Best-effort async refresh. Permission/focus rejections are
// expected in non-activated contexts — swallow them so we don't
// spam the console on every paste check.
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText()
.then((t) => { if (typeof t === "string") __clipboardCache = t; })
.catch(() => {});
}
if (__clipboardCache === null) return 0;
return __writeUtf8(__clipboardCache);
}
// ─── 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, clipboardGetText,
// History
pushState, addPopStateListener, removePopStateListener, getPathName,
// Gamepad
gamepadPollConnected, gamepadPollDisconnected,
gamepadCount, gamepadGetButton, gamepadGetAxis,
});