wasm improvements

This commit is contained in:
Jorijn van der Graaf 2026-08-05 04:08:13 +02:00
commit 6aa0338c0f
9 changed files with 603 additions and 70 deletions

View file

@ -61,7 +61,12 @@ export namespace Crafter::Dom {
// DOM ops ─────────────────────────────────────────────────────
void SetInnerHTML(const std::string_view html);
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 AddClass(const std::string_view className);
void RemoveClass(const std::string_view className);
void ToggleClass(const std::string_view className);
@ -69,28 +74,80 @@ export namespace Crafter::Dom {
std::string GetValue();
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
// passed to the matching Remove*. The destructor automatically
// removes every handler still registered, so manual removal is
// optional. Returns 0 only if registration failed at the JS
// boundary (the element was already collected). The 23 event
// types are 1:1 with CppDOM's surface.
std::int32_t AddClickListener(std::function<void(Crafter::Dom::MouseEvent)> callback);
//
// `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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
protected:

View file

@ -17,18 +17,62 @@ export namespace Crafter::Router {
// Push a new history entry. `data` is a JSON string serialized by
// the caller — the browser stores it on the entry but the popstate
// listener in V1 receives no payload (matches CppDOM's surface).
// `url` is browser-relative, e.g. "/blog/post-1".
// `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);
// 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,
// programmatic history.go). Returns an opaque id usable with
// `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);
void RemovePopStateListener(std::int32_t id);
// Current `window.location.pathname` as a freshly-allocated string.
// Allocates per call — cache the result in the caller if used in
// hot paths.
std::string GetPath();
// Location accessors. Each allocates a fresh string per call — cache
// the result if used in a hot path.
//
// 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

View file

@ -121,7 +121,22 @@ export namespace Crafter {
~Window();
#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();
// 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 StopUpdate();
void SetTitle(const std::string_view title);