browser DOM support
This commit is contained in:
parent
3859c43ce3
commit
5352ef69a2
37 changed files with 2637 additions and 59 deletions
153
examples/HelloDom/main.cpp
Normal file
153
examples/HelloDom/main.cpp
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
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;
|
||||
}
|
||||
41
examples/HelloDom/project.cpp
Normal file
41
examples/HelloDom/project.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
HelloDom — the minimum DOM-mode example. Build with:
|
||||
crafter-build --local --target=wasm32-wasip1
|
||||
|
||||
Output lands in bin/HelloDom-wasm32-wasip1-native-native-<hash>/. The
|
||||
runtime expects a co-located dom-env.js (provided by Crafter.Graphics
|
||||
itself) plus a runtime.js / index.html pair that wires the env into the
|
||||
WebAssembly imports — see EnableWasiBrowserRuntime in Crafter.Build.
|
||||
*/
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||
depArgs.push_back("--target=wasm32-wasip1");
|
||||
Configuration* graphics = LocalProject({
|
||||
.projectFile = "../../project.cpp",
|
||||
.args = depArgs,
|
||||
});
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "HelloDom";
|
||||
cfg.outputName = "HelloDom";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
cfg.target = "wasm32-wasip1";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { graphics };
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
// Generates index.html + runtime.js in the bin dir so the .wasm can
|
||||
// be loaded directly in a browser via a static file server.
|
||||
EnableWasiBrowserRuntime(cfg);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
2
examples/HelloDom/serve.sh
Executable file
2
examples/HelloDom/serve.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env sh
|
||||
caddy file-server --listen :8080 --root bin/HelloDom-wasm32-wasip1-native-native-df37fe0fe124fe57
|
||||
Loading…
Add table
Add a link
Reference in a new issue