153 lines
6.6 KiB
C++
153 lines
6.6 KiB
C++
|
|
/*
|
||
|
|
HelloDom — exercises every public surface of the DOM partition that
|
||
|
|
absorbed Crafter.CppDOM:
|
||
|
|
|
||
|
|
* `Window` as a page-level event hub (no rendering — that's V2 / WebGPU).
|
||
|
|
* `HtmlElement::CreateInBody` for element creation (new in this lib;
|
||
|
|
CppDOM was query-only).
|
||
|
|
* `HtmlElementPtr::Add*Listener` with auto-cleanup on destruction.
|
||
|
|
* `Router::PushState` / `AddPopStateListener` / `GetPath` driving a
|
||
|
|
real SPA — each route swaps the main content area without a server
|
||
|
|
round-trip.
|
||
|
|
|
||
|
|
Serve the bin dir over HTTP, open index.html, navigate between Home /
|
||
|
|
About via the in-page links, hit back/forward, watch mouse/keyboard
|
||
|
|
events log into the box at the bottom.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import Crafter.Graphics;
|
||
|
|
import Crafter.Event;
|
||
|
|
import std;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
// ─── SPA scaffolding ──────────────────────────────────────────────────
|
||
|
|
// The full app shape:
|
||
|
|
//
|
||
|
|
// [ nav: Home | About ]
|
||
|
|
// ────────────────────
|
||
|
|
// [ main — replaced per route ]
|
||
|
|
// ────────────────────
|
||
|
|
// [ log — append-only event trace ]
|
||
|
|
//
|
||
|
|
// `static` storage on the C++ side keeps the HtmlElements alive for the
|
||
|
|
// page's whole lifetime (their destructors would otherwise yank the
|
||
|
|
// nodes out of the DOM and unregister listeners when main() returns).
|
||
|
|
// Window itself is also `static` because the JS bridge stashes a raw
|
||
|
|
// pointer to it on construction; stack-local would leave that pointer
|
||
|
|
// dangling as soon as main returned.
|
||
|
|
|
||
|
|
static std::string log;
|
||
|
|
static void Append(std::string_view line) {
|
||
|
|
log += std::string(line) + "\n";
|
||
|
|
Dom::HtmlElementPtr pre("hello-log");
|
||
|
|
pre.SetInnerHTML(log);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Elements scoped to the currently-rendered route. `HtmlElementPtr`
|
||
|
|
// auto-removes its registered listeners when destroyed (the V1 fix
|
||
|
|
// for CppDOM's silent-leak class of bug), so any element we want a
|
||
|
|
// click handler on must outlive `Render()` — otherwise the destructor
|
||
|
|
// kicks in immediately and the listener dies before the user can click.
|
||
|
|
// Clearing this list at the top of every Render() detaches the previous
|
||
|
|
// route's bindings; pushing into it within a branch keeps the new
|
||
|
|
// route's bindings alive until the next navigation.
|
||
|
|
static std::vector<Dom::HtmlElementPtr> routeBindings;
|
||
|
|
|
||
|
|
static void Render(std::string_view path) {
|
||
|
|
routeBindings.clear();
|
||
|
|
Dom::HtmlElementPtr main("hello-main");
|
||
|
|
if (path == "/" || path.empty()) {
|
||
|
|
main.SetInnerHTML(R"(
|
||
|
|
<h2>Home</h2>
|
||
|
|
<p>This is the home route. Click <b>About</b> above to
|
||
|
|
navigate without reloading.</p>
|
||
|
|
<button id="hello-btn">Click me</button>
|
||
|
|
)");
|
||
|
|
Dom::HtmlElementPtr& btn = routeBindings.emplace_back("hello-btn");
|
||
|
|
btn.AddClickListener([](Dom::MouseEvent e) {
|
||
|
|
Append(std::format("button click @ ({:.0f},{:.0f})", e.clientX, e.clientY));
|
||
|
|
});
|
||
|
|
} else if (path == "/about") {
|
||
|
|
main.SetInnerHTML(R"(
|
||
|
|
<h2>About</h2>
|
||
|
|
<p>HelloDom is the demo for Crafter.Graphics's
|
||
|
|
<code>CRAFTER_GRAPHICS_WINDOW_DOM</code> mode. The whole
|
||
|
|
page — content, routing, event handling — is compiled
|
||
|
|
C++ running in WebAssembly.</p>
|
||
|
|
)");
|
||
|
|
} else {
|
||
|
|
main.SetInnerHTML(std::format(R"(
|
||
|
|
<h2>Not found</h2>
|
||
|
|
<p>No route registered for <code>{}</code>.</p>
|
||
|
|
)", path));
|
||
|
|
}
|
||
|
|
Append(std::format("rendered route: {}", path));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Used by both the nav click handlers and (indirectly) the popstate
|
||
|
|
// listener. Mutates browser history, then re-renders.
|
||
|
|
static void Navigate(std::string_view url) {
|
||
|
|
Router::PushState("{}", "", url);
|
||
|
|
Render(url);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
Device::Initialize();
|
||
|
|
static Window window(0, 0, "HelloDom");
|
||
|
|
|
||
|
|
// Build the persistent page chrome. The route links use plain
|
||
|
|
// <span class="route-link"> (no <a href>) so the browser does NOT
|
||
|
|
// navigate when they're clicked — only our handler runs. This is
|
||
|
|
// the canonical SPA pattern; if you want right-click-open-in-new-tab
|
||
|
|
// you'd switch to <a href> and call preventDefault in a custom JS
|
||
|
|
// intercept (not exposed in V1).
|
||
|
|
static Dom::HtmlElement root = Dom::HtmlElement::CreateInBody("div", "hello-root");
|
||
|
|
root.SetStyle("font-family:system-ui,sans-serif;padding:24px;max-width:640px;");
|
||
|
|
root.SetInnerHTML(R"(
|
||
|
|
<nav style="margin-bottom:16px">
|
||
|
|
<span id="link-home" class="route-link" style="cursor:pointer;text-decoration:underline;margin-right:12px">Home</span>
|
||
|
|
<span id="link-about" class="route-link" style="cursor:pointer;text-decoration:underline">About</span>
|
||
|
|
</nav>
|
||
|
|
<div id="hello-main" style="min-height:120px;padding:12px;border:1px solid #ddd;border-radius:6px"></div>
|
||
|
|
<h3 style="margin-top:24px;font-size:14px;text-transform:uppercase;color:#666">Event log</h3>
|
||
|
|
<pre id="hello-log" style="background:#eee;padding:8px;height:160px;overflow:auto;font-size:12px"></pre>
|
||
|
|
)");
|
||
|
|
|
||
|
|
// Nav link click handlers. `static` so they outlive main(); the
|
||
|
|
// <span>s themselves are persistent (we never SetInnerHTML on
|
||
|
|
// <nav>), so the same listener fires for every nav click.
|
||
|
|
static Dom::HtmlElementPtr linkHome("link-home");
|
||
|
|
static Dom::HtmlElementPtr linkAbout("link-about");
|
||
|
|
linkHome .AddClickListener([](Dom::MouseEvent) { Navigate("/"); });
|
||
|
|
linkAbout.AddClickListener([](Dom::MouseEvent) { Navigate("/about"); });
|
||
|
|
|
||
|
|
// Browser back / forward: re-render whatever the URL now points to.
|
||
|
|
Router::AddPopStateListener([] {
|
||
|
|
std::string path = Router::GetPath();
|
||
|
|
Append(std::format("popstate → {}", path));
|
||
|
|
Render(path);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Initial render — boot into whatever path the page loaded with.
|
||
|
|
Append("page initialised");
|
||
|
|
Render(Router::GetPath());
|
||
|
|
|
||
|
|
// Page-level events come through Window. KeyDown fires for every
|
||
|
|
// key press regardless of focus; matches Wayland / Win32 semantics.
|
||
|
|
static EventListener<KeyCode> keyDownSub(&window.onRawKeyDown,
|
||
|
|
[](KeyCode code) {
|
||
|
|
Append(std::format("key down: 0x{:x}", code));
|
||
|
|
});
|
||
|
|
static EventListener<std::uint32_t> scrollSub(&window.onMouseScroll,
|
||
|
|
[](std::uint32_t delta) {
|
||
|
|
Append(std::format("scroll: {}", static_cast<std::int32_t>(delta)));
|
||
|
|
});
|
||
|
|
static EventListener<void> resizeSub(&window.onResize,
|
||
|
|
[]{ Append("resize"); });
|
||
|
|
|
||
|
|
// Kick off the rAF loop. Returns immediately on DOM; the wasm
|
||
|
|
// module stays alive while the JS bridge ticks frames.
|
||
|
|
window.StartUpdate();
|
||
|
|
window.StartSync();
|
||
|
|
return 0;
|
||
|
|
}
|