Last active
July 23, 2020 21:51
-
-
Save nikki93/05360e2306308e51daebe3227357cee0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| 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