Crafter.Graphics/examples/HelloDom/main.cpp

153 lines
6.6 KiB
C++
Raw Permalink Normal View History

2026-05-18 02:07:48 +02:00
/*
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;
}