browser DOM support

This commit is contained in:
Jorijn van der Graaf 2026-05-18 02:07:48 +02:00
commit 5352ef69a2
37 changed files with 2637 additions and 59 deletions

View file

@ -675,6 +675,161 @@ void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
}
#endif
// ────────────────────────────────────────────────────────────────────
// DOM backend (browser Gamepad API via env.js polling helpers)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
namespace Crafter::DomEnv {
// External linkage required for the import_module attribute to bind
// against the env.js function. An anonymous namespace would drop the
// attribute at link time.
__attribute__((import_module("env"), import_name("gamepadPollConnected")))
bool gamepadPollConnected();
__attribute__((import_module("env"), import_name("gamepadPollDisconnected")))
bool gamepadPollDisconnected();
__attribute__((import_module("env"), import_name("gamepadCount")))
std::int32_t gamepadCount();
__attribute__((import_module("env"), import_name("gamepadGetButton")))
std::int32_t gamepadGetButton(std::int32_t idx, std::int32_t buttonIdx);
__attribute__((import_module("env"), import_name("gamepadGetAxis")))
double gamepadGetAxis(std::int32_t idx, std::int32_t axisIdx);
}
namespace {
using Crafter::DomEnv::gamepadPollConnected;
using Crafter::DomEnv::gamepadPollDisconnected;
using Crafter::DomEnv::gamepadCount;
using Crafter::DomEnv::gamepadGetButton;
using Crafter::DomEnv::gamepadGetAxis;
// Standard W3C Gamepad mapping: indices match the "standard" layout
// every browser exposes for Xbox / DualShock / DualSense controllers.
// Mismatched / non-standard pads fall through with zeroed buttons.
constexpr int kStdBtnSouth = 0;
constexpr int kStdBtnEast = 1;
constexpr int kStdBtnWest = 2;
constexpr int kStdBtnNorth = 3;
constexpr int kStdBtnLB = 4;
constexpr int kStdBtnRB = 5;
constexpr int kStdBtnLT = 6;
constexpr int kStdBtnRT = 7;
constexpr int kStdBtnSelect = 8;
constexpr int kStdBtnStart = 9;
constexpr int kStdBtnLStickClick = 10;
constexpr int kStdBtnRStickClick = 11;
constexpr int kStdBtnDPadUp = 12;
constexpr int kStdBtnDPadDown = 13;
constexpr int kStdBtnDPadLeft = 14;
constexpr int kStdBtnDPadRight = 15;
constexpr int kStdBtnHome = 16;
constexpr int kStdAxisLX = 0, kStdAxisLY = 1, kStdAxisRX = 2, kStdAxisRY = 3;
// Map our `Gamepad::Button` ordinal to the W3C standard button index.
int StdButtonIdx(Crafter::Gamepad::Button b) {
using B = Crafter::Gamepad::Button;
switch (b) {
case B::South: return kStdBtnSouth;
case B::East: return kStdBtnEast;
case B::West: return kStdBtnWest;
case B::North: return kStdBtnNorth;
case B::Select: return kStdBtnSelect;
case B::Start: return kStdBtnStart;
case B::Home: return kStdBtnHome;
case B::LeftStickClick: return kStdBtnLStickClick;
case B::RightStickClick: return kStdBtnRStickClick;
case B::LeftBumper: return kStdBtnLB;
case B::RightBumper: return kStdBtnRB;
case B::DPadUp: return kStdBtnDPadUp;
case B::DPadDown: return kStdBtnDPadDown;
case B::DPadLeft: return kStdBtnDPadLeft;
case B::DPadRight: return kStdBtnDPadRight;
case B::LeftTrigger: return kStdBtnLT;
case B::RightTrigger: return kStdBtnRT;
default: return -1;
}
}
}
void Crafter::Gamepad::Tick() {
// Handle connect/disconnect by rebuilding `connected` from scratch
// when the JS flags say something changed. Cheap — only fires on
// actual hot-plug, not every frame.
if (gamepadPollConnected() || gamepadPollDisconnected() || connected.empty()) {
// Snapshot previously-known ids so we can fire onDisconnected
// for ones that went away in this rebuild.
std::vector<std::uint32_t> previousIds;
previousIds.reserve(connected.size());
for (auto& d : connected) previousIds.push_back(d->id);
std::int32_t n = gamepadCount();
connected.clear();
for (std::int32_t i = 0; i < n; ++i) {
auto dev = std::make_unique<Device>();
dev->id = static_cast<std::uint32_t>(i); // index-based; stable for the page
dev->name = "Gamepad";
connected.push_back(std::move(dev));
}
// Fire connected events for ids that weren't in the previous set.
for (auto& d : connected) {
bool wasKnown = false;
for (std::uint32_t prev : previousIds) if (prev == d->id) { wasKnown = true; break; }
if (!wasKnown) onConnected.Invoke(d.get());
}
// No way to call onDisconnected with the right Device* once it's
// gone — fire with nullptr so subscribers know SOMETHING changed.
for (std::uint32_t prev : previousIds) {
bool stillThere = false;
for (auto& d : connected) if (d->id == prev) { stillThere = true; break; }
if (!stillThere) onDisconnected.Invoke(nullptr);
}
}
// Poll buttons + axes on every Tick — same shape as the native paths.
for (auto& devUp : connected) {
Device& dev = *devUp;
std::int32_t idx = static_cast<std::int32_t>(dev.id);
for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) {
int stdIdx = StdButtonIdx(static_cast<Button>(b));
bool newState = stdIdx >= 0 && gamepadGetButton(idx, stdIdx) != 0;
if (newState != dev.buttons[b]) {
dev.buttons[b] = newState;
if (newState) dev.onButtonDown.Invoke(static_cast<Button>(b));
else dev.onButtonUp .Invoke(static_cast<Button>(b));
}
}
// Axes — sticks come straight through, triggers come from button
// analog values (the standard mapping exposes those too, with
// buttons[6/7].value, but the polling shim returns binary
// pressed-state. For V1, triggers report 0 / 1 only).
float lx = static_cast<float>(gamepadGetAxis(idx, kStdAxisLX));
float ly = static_cast<float>(gamepadGetAxis(idx, kStdAxisLY));
float rx = static_cast<float>(gamepadGetAxis(idx, kStdAxisRX));
float ry = static_cast<float>(gamepadGetAxis(idx, kStdAxisRY));
auto setAxis = [&](Axis a, float v) {
std::size_t i = (std::size_t)a;
if (dev.axes[i] != v) {
dev.axes[i] = v;
dev.onAxisChanged.Invoke(a);
}
};
setAxis(Axis::LeftStickX, lx);
setAxis(Axis::LeftStickY, ly);
setAxis(Axis::RightStickX, rx);
setAxis(Axis::RightStickY, ry);
setAxis(Axis::LeftTrigger, gamepadGetButton(idx, kStdBtnLT) ? 1.0f : 0.0f);
setAxis(Axis::RightTrigger, gamepadGetButton(idx, kStdBtnRT) ? 1.0f : 0.0f);
}
}
void Crafter::Gamepad::Rumble(Device& /*dev*/, float /*low*/, float /*high*/,
std::chrono::milliseconds /*duration*/) {
// Browser Gamepad rumble (HapticActuator API) is patchy across
// engines. V1: no-op. Pads that don't support it would silently
// fall through anyway.
}
#endif
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
for (auto& up : connected) {
if (up->id == id) return up.get();