Skip to content

Instantly share code, notes, and snippets.

@nikki93
Last active July 23, 2020 21:51
Show Gist options
  • Select an option

  • Save nikki93/05360e2306308e51daebe3227357cee0 to your computer and use it in GitHub Desktop.

Select an option

Save nikki93/05360e2306308e51daebe3227357cee0 to your computer and use it in GitHub Desktop.
#include "ui.h"
#include "debug.h"
// JS binding
// Stub `EM_JS` when not in Emscripten
#ifndef __EMSCRIPTEN__
#define EM_JS(retType, name, ...) \
template<typename... Args> \
inline retType name(Args &&... args) { \
(args, ...); \
return retType(); \
}
#endif
// Same as `EM_JS`, but adds a `JS_` prefix to prevent clashes
#define JS_DEFINE(retType, name, args, ...) EM_JS(retType, JS_##name, args, __VA_ARGS__)
// Allows passing `void((void))` to JS
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_BINDINGS(VoidFunctor) {
Debug::ignoreLeaks([&]() { // Leak in `getContext` from 'emscripten/bind.h'
emscripten::class_<std::function<void(void)>>("VoidFunctor")
.constructor<>()
.function("opcall", &std::function<void(void)>::operator());
});
}
#endif
// Constructor, destructor
UI::UI(const Timing &tim_)
: tim(tim_)
, lastUiUpdate(tim.t()) {
}
// Elem open / close
JS_DEFINE(void, elementOpenStart, (const char *tag), {
IncrementalDOM.elementOpenStart(UTF8ToString(tag));
void(0);
})
JS_DEFINE(void, elementOpenStartKeyInt, (const char *tag, int key), {
IncrementalDOM.elementOpenStart(UTF8ToString(tag), key.toString());
void(0);
})
JS_DEFINE(void, elementOpenStartKeyString, (const char *tag, const char *key), {
IncrementalDOM.elementOpenStart(UTF8ToString(tag), UTF8ToString(key));
void(0);
})
UI::Elem::Elem(UI &ui_, const char *tag_)
: ui(ui_)
, tag(tag_) {
if (std::holds_alternative<std::monostate>(ui.nextKey)) {
JS_elementOpenStart(tag);
} else if (std::holds_alternative<int>(ui.nextKey)) {
JS_elementOpenStartKeyInt(tag, std::get<int>(ui.nextKey));
ui.nextKey = {};
} else if (std::holds_alternative<const char *>(ui.nextKey)) {
JS_elementOpenStartKeyString(tag, std::get<const char *>(ui.nextKey));
ui.nextKey = {};
}
}
JS_DEFINE(void, elementClose, (const char *tag), {
IncrementalDOM.elementClose(UTF8ToString(tag));
void(0);
})
UI::Elem::~Elem() {
if (tag) {
end();
JS_elementClose(tag);
}
}
JS_DEFINE(void, elementOpenEnd, (), {
IncrementalDOM.elementOpenEnd();
void(0);
})
auto UI::Elem::end() -> void {
if (!ended) {
ended = true;
JS_elementOpenEnd();
}
}
// Elem attributes
JS_DEFINE(void, attrInt, (const char *name, int value), {
IncrementalDOM.attr(UTF8ToString(name), value);
void(0);
})
auto UI::Elem::operator()(const char *name, int value) -> Elem & {
JS_attrInt(name, value);
return *this;
}
JS_DEFINE(void, attrDouble, (const char *name, double value), {
IncrementalDOM.attr(UTF8ToString(name), value);
void(0);
})
auto UI::Elem::operator()(const char *name, double value) -> Elem & {
JS_attrDouble(name, value);
return *this;
}
JS_DEFINE(void, attrBool, (const char *name, bool value), {
if (value) {
IncrementalDOM.attr(UTF8ToString(name), "");
}
void(0);
})
auto UI::Elem::operator()(const char *name, bool value) -> Elem & {
JS_attrBool(name, value);
return *this;
}
JS_DEFINE(void, attrString, (const char *name, const char *value), {
IncrementalDOM.attr(UTF8ToString(name), UTF8ToString(value));
void(0);
})
auto UI::Elem::operator()(const char *name, const char *value) -> Elem & {
JS_attrString(name, value);
return *this;
}
auto UI::Elem::operator()(const char *name, const std::string &value) -> Elem & {
JS_attrString(name, value.c_str());
return *this;
}
// Text
JS_DEFINE(void, text, (const char *value), {
IncrementalDOM.text(UTF8ToString(value));
void(0);
})
auto UI::text(const char *value) -> void {
JS_text(value);
}
// Layout
auto UI::div() -> Elem {
return elem("div");
}
// Controls
auto UI::button() -> Elem {
return elem("button");
}
// Top-level
// NOLINTNEXTLINE(modernize-use-trailing-return-type)
JS_DEFINE(int, getEventCount, (const char *type), {
void(0);
return UI.getEventCount(UTF8ToString(type));
})
auto UI::getEventCount(const char *type) -> int {
return JS_getEventCount(type);
}
JS_DEFINE(void, clearEvents, (), {
UI.clearEvents();
void(0);
})
auto UI::clearEvents() -> void {
JS_clearEvents();
}
#pragma once
#include "precomp.h"
#include "timing.h"
struct UI {
UI(const UI &) = delete;
auto operator=(const UI &) -> UI & = delete;
UI(const UI &&) = delete;
auto operator=(const UI &&) -> UI & = delete;
explicit UI(const Timing &tim_);
~UI() = default;
// Elem
struct Elem {
Elem(const Elem &) = delete;
auto operator=(const Elem &) -> Elem & = delete;
Elem(Elem &&) noexcept;
auto operator=(Elem &&) noexcept -> Elem &;
~Elem();
// Attributes
auto operator()(const char *klass) -> Elem &;
auto operator()(const char *name, int value) -> Elem &;
auto operator()(const char *name, double value) -> Elem &;
auto operator()(const char *name, bool value) -> Elem &;
auto operator()(const char *name, const char *value) -> Elem &;
auto operator()(const char *name, const std::string &value) -> Elem &;
// Events
template<typename F>
auto operator()(const char *type, F &&f) -> Elem &;
// Children
template<typename F>
auto operator()(F &&f) -> void;
private:
friend struct UI;
UI &ui;
const char *tag = nullptr;
bool ended = false;
explicit Elem(UI &ui, const char *tag_);
auto end() -> void;
};
auto elem(const char *tag) -> Elem;
using Key = std::variant<std::monostate, int, const char *>;
auto key(Key key_) -> UI &;
// Text
auto text(const char *value) -> void;
template<typename S, typename... Args>
auto text(S &&s, Args &&... args) -> void;
// Layout
auto div() -> Elem;
// Controls
auto button() -> Elem;
// Top-level
template<typename F>
auto panel(const std::string &id, F &&f) -> void;
template<typename F>
auto frame(F &&f) -> void;
private:
friend struct Elem;
const Timing &tim;
Key nextKey;
double lastUiUpdate = 0;
static auto getEventCount(const char *type) -> int;
auto clearEvents() -> void;
template<typename F>
static auto bind(F &&f) -> emscripten::val {
std::function<void(void)> functor(f);
return emscripten::val(functor);
}
template<typename T, typename F>
auto patch(T &target, F &&f) -> void {
emscripten::val::global("UI").call<void>("patch", target, bind(std::forward<F>(f)));
}
};
// Elem
inline UI::Elem::Elem(Elem &&other) noexcept
: ui(other.ui)
, tag(std::exchange(other.tag, nullptr)) {
}
inline auto UI::Elem::operator=(Elem &&other) noexcept -> Elem & {
if (this != &other) {
tag = std::exchange(other.tag, nullptr);
}
return *this;
}
inline auto UI::Elem::operator()(const char *klass) -> Elem & {
return operator()("class", klass);
}
template<typename F>
auto UI::Elem::operator()(const char *type, F &&f) -> Elem & {
static_assert(std::is_invocable_v<F, emscripten::val>,
"event handler must take a single `emscripten::val` parameter");
end();
auto count = getEventCount(type);
if (count > 0) {
auto target = emscripten::val::global("IncrementalDOM").call<emscripten::val>("currentElement");
for (auto i = 0; i < count; ++i) {
f(target);
}
}
return *this;
}
template<typename F>
auto UI::Elem::operator()(F &&f) -> void {
end();
f();
}
inline auto UI::elem(const char *tag) -> Elem {
return Elem(*this, tag);
}
inline auto UI::key(Key key_) -> UI & {
nextKey = key_;
return *this;
}
// Text
template<typename S, typename... Args>
auto UI::text(S &&s, Args &&... args) -> void {
auto formatted = fmt::format(std::forward<S>(s), std::forward<Args>(args)...);
text(formatted.c_str());
}
// Top-level
template<typename F>
auto UI::panel(const std::string &id, F &&f) -> void {
auto target = emscripten::val::global("document").call<emscripten::val>("getElementById", id);
patch(target, f);
}
template<typename F>
auto UI::frame([[maybe_unused]] F &&f) -> void {
#ifdef __EMSCRIPTEN__
if (tim.t() - lastUiUpdate > 1 / 20.0) {
lastUiUpdate = tim.t();
f();
clearEvents();
}
#endif
}
(() => {
window.UI = {};
// Map of DOM element -> event type -> number of times event was fired
// since last event clear
let eventCounts = new WeakMap();
// Common handler for all elements -- just increments count in above map
const handler = (e) => {
const target = e.target;
let counts = eventCounts.get(target);
if (counts === undefined) {
counts = {};
eventCounts.set(target, counts);
}
const count = counts[e.type];
if (count === undefined) {
counts[e.type] = 1;
} else {
counts[e.type] = count + 1;
}
};
// Get number the number of times the event of the given type was fired
// on the current element since the last event clear. Also binds the
// common event handler if not bound yet.
window.UI.getEventCount = (type) => {
const target = IncrementalDOM.currentElement();
// Register handler if not already registered
if (!target.__UIHandlerRegistered) {
target.addEventListener(type, handler);
target.__UIHandlerRegistered = true;
}
// Return count
const counts = eventCounts.get(target);
if (counts === undefined) {
return 0;
}
const count = counts[type];
if (count === undefined) {
return 0;
}
return count;
};
// Reset `eventCounts`
window.UI.clearEvents = () => {
eventCounts = new WeakMap();
};
// Run `IncrementalDOM.patch` and free the functor
window.UI.patch = (target, f) => {
IncrementalDOM.patch(target, () => f.opcall());
f.delete();
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment