Crafter.Graphics/interfaces/Crafter.Graphics-Dom.cppm

269 lines
14 KiB
Text
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-05-18 02:07:48 +02:00
// Browser DOM bindings — absorbed from Crafter.CppDOM. Only meaningful
// when the build defined CRAFTER_GRAPHICS_WINDOW_DOM (i.e. a wasm32-*
// target paired with additional/dom-env.js); on native platforms the
// partition still parses but every method links to nothing because the
// build excludes the impl. project.cpp's per-target module set is what
// makes the partition disappear from the native libs.
//
// Two-class hierarchy (simplified from CppDOM's three-class design):
// HtmlElementPtr — non-owning element reference. Tracks every handler
// it registered and clears them on destruction, so
// the silent-leak bug from CppDOM's bare-pointer
// flavour can't happen. Releases the JS handle on
// destruction.
// HtmlElement — `HtmlElementPtr` plus element ownership: removes
// the element from the DOM on destruction. The new
// `Create` factory builds a fresh element under a
// parent and returns one of these.
//
// Move-only throughout. The JS handle (`ptr`) of a moved-from instance
// is zeroed so the destructor of the moved-from carcass is a no-op.
export module Crafter.Graphics:Dom;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :DomEvents;
export namespace Crafter::Dom {
class HtmlElementPtr {
public:
// Opaque JS-side element cookie. 0 = "no element"; the destructor
// treats 0 as "moved-from, nothing to release".
std::int32_t ptr;
// Look up an existing element by its DOM id.
HtmlElementPtr(const std::string_view id);
// Look up + replace innerHTML in one step (matches CppDOM).
HtmlElementPtr(const std::string_view id, const std::string_view html);
protected:
// Adopt a JS handle directly (used by HtmlElement::Create — the
// JS side has already minted the handle, no second lookup needed).
// Tagged-type ctor so the public id-based overloads stay
// unambiguous from the call site.
struct FromHandle { std::int32_t handle; };
explicit HtmlElementPtr(FromHandle h) noexcept : ptr(h.handle) {}
public:
// Move-only — copying would silently double-free the JS handle.
HtmlElementPtr(HtmlElementPtr&&) noexcept;
HtmlElementPtr& operator=(HtmlElementPtr&&) noexcept;
HtmlElementPtr(const HtmlElementPtr&) = delete;
HtmlElementPtr& operator=(const HtmlElementPtr&) = delete;
~HtmlElementPtr();
// DOM ops ─────────────────────────────────────────────────────
void SetInnerHTML(const std::string_view html);
void SetStyle(const std::string_view style);
2026-08-05 04:08:13 +02:00
// 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.
2026-05-18 02:07:48 +02:00
void SetProperty(const std::string_view property, const std::string_view value);
2026-08-05 04:08:13 +02:00
2026-05-18 02:07:48 +02:00
void AddClass(const std::string_view className);
void RemoveClass(const std::string_view className);
void ToggleClass(const std::string_view className);
bool HasClass(const std::string_view className);
std::string GetValue();
void SetValue(const std::string_view value);
2026-08-05 04:08:13 +02:00
// 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();
2026-05-18 02:07:48 +02:00
// Listener API — each Add* returns an opaque id that can be
// passed to the matching Remove*. The destructor automatically
// removes every handler still registered, so manual removal is
// optional. Returns 0 only if registration failed at the JS
// boundary (the element was already collected). The 23 event
// types are 1:1 with CppDOM's surface.
2026-08-05 04:08:13 +02:00
//
// `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);
2026-05-18 02:07:48 +02:00
void RemoveClickListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddMouseOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveMouseOverListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddMouseOutListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveMouseOutListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddMouseMoveListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveMouseMoveListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddMouseDownListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveMouseDownListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddMouseUpListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveMouseUpListener(std::int32_t id);
std::int32_t AddFocusListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
void RemoveFocusListener(std::int32_t id);
std::int32_t AddBlurListener(std::function<void(Crafter::Dom::FocusEvent)> callback);
void RemoveBlurListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddKeyDownListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveKeyDownListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddKeyUpListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveKeyUpListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddKeyPressListener(std::function<void(Crafter::Dom::KeyboardEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveKeyPressListener(std::int32_t id);
std::int32_t AddChangeListener(std::function<void(Crafter::Dom::ChangeEvent)> callback);
void RemoveChangeListener(std::int32_t id);
std::int32_t AddSubmitListener(std::function<void()> callback);
void RemoveSubmitListener(std::int32_t id);
std::int32_t AddInputListener(std::function<void(Crafter::Dom::InputEvent)> callback);
void RemoveInputListener(std::int32_t id);
std::int32_t AddResizeListener(std::function<void(Crafter::Dom::ResizeEvent)> callback);
void RemoveResizeListener(std::int32_t id);
std::int32_t AddScrollListener(std::function<void(Crafter::Dom::ScrollEvent)> callback);
void RemoveScrollListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddContextMenuListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveContextMenuListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDragStartListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDragStartListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDragEndListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDragEndListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDropListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDropListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDragOverListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDragOverListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDragEnterListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDragEnterListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddDragLeaveListener(std::function<void(Crafter::Dom::MouseEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveDragLeaveListener(std::int32_t id);
2026-08-05 04:08:13 +02:00
std::int32_t AddWheelListener(std::function<void(Crafter::Dom::WheelEvent)> callback,
bool preventDefault = false);
2026-05-18 02:07:48 +02:00
void RemoveWheelListener(std::int32_t id);
protected:
// Per-event-kind handler-id lists. Each `Add*Listener` push_backs
// the id it returns; `RemoveAllHandlers` walks every list and
// calls the matching JS Remove on each id. The lists are indexed
// by event kind so a single handler-id collision across kinds
// (allowed by the JS bridge — counters are per-event-kind)
// doesn't cause a wrong-kind remove.
std::vector<std::int32_t> handlerIds_[24];
// Shared cleanup used by the destructor AND by move-assignment.
// Removes every registered handler from both the C++ map and
// the JS side, then frees the JS handle. Leaves `ptr == 0` on
// return so a second call is a no-op.
void RemoveAllHandlersAndFree();
};
class HtmlElement : public HtmlElementPtr {
public:
// Adopt an existing element by id; the dtor will remove it from
// the DOM. Useful when the caller created the element in markup
// and wants C++ to own its lifetime from now on.
HtmlElement(const std::string_view id);
HtmlElement(const std::string_view id, const std::string_view html);
HtmlElement(HtmlElement&&) noexcept;
HtmlElement& operator=(HtmlElement&&) noexcept;
~HtmlElement();
// Create a new element and append it to `parent`. Optional `id`
// sets the new element's DOM id (use it when you want to look the
// element up from non-C++ code later); empty leaves the element
// unnamed. Returns an owning handle — the element is removed from
// the DOM when this `HtmlElement` is destroyed.
static HtmlElement Create(const HtmlElementPtr& parent,
std::string_view tagName,
std::string_view id = {});
// Convenience: create a top-level element under document.body.
static HtmlElement CreateInBody(std::string_view tagName,
std::string_view id = {});
private:
// Hidden ctor used by Create/CreateInBody — takes an already
// allocated JS handle via the base class FromHandle tag. Kept
// private so user code doesn't construct an HtmlElement from a
// raw handle by accident.
explicit HtmlElement(HtmlElementPtr::FromHandle h) noexcept
: HtmlElementPtr(h) {}
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM