browser DOM support
This commit is contained in:
parent
3859c43ce3
commit
5352ef69a2
37 changed files with 2637 additions and 59 deletions
|
|
@ -98,8 +98,22 @@ namespace {
|
|||
}
|
||||
#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) {
|
||||
#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) {
|
||||
// Compositor doesn't expose wl_data_device_manager (rare; some
|
||||
// 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
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// 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) {
|
||||
for (auto& up : connected) {
|
||||
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 <cassert>
|
||||
#endif
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
#include "vulkan/vulkan_wayland.h"
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
#include "vulkan/vulkan_win32.h"
|
||||
#endif
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "../lib/stb_image_write.h"
|
||||
#endif
|
||||
module Crafter.Graphics:Window_impl;
|
||||
import :Window;
|
||||
import :Device;
|
||||
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 :DescriptorHeapVulkan;
|
||||
import :RenderPass;
|
||||
|
|
@ -64,7 +73,7 @@ import std;
|
|||
|
||||
using namespace Crafter;
|
||||
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
void randname(char *buf) {
|
||||
struct timespec ts;
|
||||
|
|
@ -1246,4 +1255,277 @@ void Window::SaveFrame(const std::filesystem::path& path) {
|
|||
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
|
||||
vkDestroyBuffer(Device::device, stagingBuf, 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue