//-----------------------------------------------------------------------------
// The Emscripten-based implementation of platform-dependent GUI functionality.
//
// Copyright 2018 whitequark
//-----------------------------------------------------------------------------
#include
#include
#include
#include "config.h"
#include "solvespace.h"
using namespace emscripten;
namespace SolveSpace {
namespace Platform {
//-----------------------------------------------------------------------------
// Emscripten API bridging
//-----------------------------------------------------------------------------
#define sscheck(expr) do { \
EMSCRIPTEN_RESULT emResult = (EMSCRIPTEN_RESULT)(expr); \
if(emResult < 0) \
HandleError(__FILE__, __LINE__, __func__, #expr, emResult); \
} while(0)
static void HandleError(const char *file, int line, const char *function, const char *expr,
EMSCRIPTEN_RESULT emResult) {
const char *error = "Unknown error";
switch(emResult) {
case EMSCRIPTEN_RESULT_DEFERRED: error = "Deferred"; break;
case EMSCRIPTEN_RESULT_NOT_SUPPORTED: error = "Not supported"; break;
case EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED: error = "Failed (not deferred)"; break;
case EMSCRIPTEN_RESULT_INVALID_TARGET: error = "Invalid target"; break;
case EMSCRIPTEN_RESULT_UNKNOWN_TARGET: error = "Unknown target"; break;
case EMSCRIPTEN_RESULT_INVALID_PARAM: error = "Invalid parameter"; break;
case EMSCRIPTEN_RESULT_FAILED: error = "Failed"; break;
case EMSCRIPTEN_RESULT_NO_DATA: error = "No data"; break;
}
std::string message;
message += ssprintf("File %s, line %u, function %s:\n", file, line, function);
message += ssprintf("Emscripten API call failed: %s.\n", expr);
message += ssprintf("Error: %s\n", error);
FatalError(message);
}
static val Wrap(const std::string& str) {
// FIXME(emscripten): a nicer way to do this?
EM_ASM($Wrap$ret = UTF8ToString($0), str.c_str());
return val::global("window")["$Wrap$ret"];
}
static std::string Unwrap(val emStr) {
// FIXME(emscripten): a nicer way to do this?
val emArray = val::global("window").call("intArrayFromString", emStr, true) ;
val::global("window").set("$Wrap$input", emArray);
char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, ALLOC_NORMAL));
std::string str(strC, emArray["length"].as());
free(strC);
return str;
}
static void CallStdFunction(void *data) {
std::function *func = (std::function *)data;
if(*func) {
(*func)();
}
}
static val Wrap(std::function *func) {
EM_ASM($Wrap$ret = Module.dynCall_vi.bind(null, $0, $1), CallStdFunction, func);
return val::global("window")["$Wrap$ret"];
}
//-----------------------------------------------------------------------------
// Fatal errors
//-----------------------------------------------------------------------------
void FatalError(const std::string &message) {
dbp("%s", message.c_str());
#ifndef NDEBUG
emscripten_debugger();
#endif
abort();
}
//-----------------------------------------------------------------------------
// Settings
//-----------------------------------------------------------------------------
class SettingsImplHtml : public Settings {
public:
void FreezeInt(const std::string &key, uint32_t value) {
val::global("localStorage").call("setItem", Wrap(key), value);
}
uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) {
val value = val::global("localStorage").call("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return val::global("parseInt")(value, 0).as();
}
void FreezeFloat(const std::string &key, double value) {
val::global("localStorage").call("setItem", Wrap(key), value);
}
double ThawFloat(const std::string &key, double defaultValue = 0.0) {
val value = val::global("localStorage").call("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return val::global("parseFloat")(value).as();
}
void FreezeString(const std::string &key, const std::string &value) {
val::global("localStorage").call("setItem", Wrap(key), value);
}
std::string ThawString(const std::string &key,
const std::string &defaultValue = "") {
val value = val::global("localStorage").call("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return Unwrap(value);
}
};
SettingsRef GetSettings() {
return std::make_shared();
}
//-----------------------------------------------------------------------------
// Timers
//-----------------------------------------------------------------------------
class TimerImplHtml : public Timer {
public:
static void Callback(void *arg) {
TimerImplHtml *timer = (TimerImplHtml *)arg;
if(timer->onTimeout) {
timer->onTimeout();
}
}
void RunAfter(unsigned milliseconds) override {
emscripten_async_call(TimerImplHtml::Callback, this, milliseconds + 1);
}
void RunAfterNextFrame() override {
emscripten_async_call(TimerImplHtml::Callback, this, 0);
}
void RunAfterProcessingEvents() override {
emscripten_push_uncounted_main_loop_blocker(TimerImplHtml::Callback, this);
}
};
TimerRef CreateTimer() {
return std::unique_ptr(new TimerImplHtml);
}
//-----------------------------------------------------------------------------
// Menus
//-----------------------------------------------------------------------------
class MenuItemImplHtml : public MenuItem {
public:
val htmlMenuItem;
MenuItemImplHtml() :
htmlMenuItem(val::global("document").call("createElement", val("li")))
{}
void SetAccelerator(KeyboardEvent accel) override {
val htmlAccel = htmlMenuItem.call("querySelector", val(".accel"));
if(htmlAccel.as()) {
htmlAccel.call("remove");
}
htmlAccel = val::global("document").call("createElement", val("span"));
htmlAccel.call("setAttribute", val("class"), val("accel"));
htmlAccel.set("innerText", AcceleratorDescription(accel));
htmlMenuItem.call("appendChild", htmlAccel);
}
void SetIndicator(Indicator type) override {
val htmlClasses = htmlMenuItem["classList"];
htmlClasses.call("remove", val("check"));
htmlClasses.call("remove", val("radio"));
switch(type) {
case Indicator::NONE:
break;
case Indicator::CHECK_MARK:
htmlClasses.call("add", val("check"));
break;
case Indicator::RADIO_MARK:
htmlClasses.call("add", val("radio"));
break;
}
}
void SetEnabled(bool enabled) override {
if(enabled) {
htmlMenuItem["classList"].call("remove", val("disabled"));
} else {
htmlMenuItem["classList"].call("add", val("disabled"));
}
}
void SetActive(bool active) override {
if(active) {
htmlMenuItem["classList"].call("add", val("active"));
} else {
htmlMenuItem["classList"].call("remove", val("active"));
}
}
};
class MenuImplHtml;
static std::shared_ptr popupMenuOnScreen;
class MenuImplHtml : public Menu,
public std::enable_shared_from_this {
public:
val htmlMenu;
std::vector<:shared_ptr>> menuItems;
std::vector<:shared_ptr>> subMenus;
std::function popupDismissFunc;
MenuImplHtml() :
htmlMenu(val::global("document").call("createElement", val("ul")))
{
htmlMenu["classList"].call("add", val("menu"));
}
MenuItemRef AddItem(const std::string &label, std::function onTrigger,
bool mnemonics = true) override {
std::shared_ptr menuItem = std::make_shared();
menuItems.push_back(menuItem);
menuItem->onTrigger = onTrigger;
if(mnemonics) {
val::global("window").call("setLabelWithMnemonic", menuItem->htmlMenuItem,
Wrap(label));
} else {
val htmlLabel = val::global("document").call("createElement", val("span"));
htmlLabel["classList"].call("add", val("label"));
htmlLabel.set("innerText", Wrap(label));
menuItem->htmlMenuItem.call("appendChild", htmlLabel);
}
menuItem->htmlMenuItem.call("addEventListener", val("trigger"),
Wrap(&menuItem->onTrigger));
htmlMenu.call("appendChild", menuItem->htmlMenuItem);
return menuItem;
}
std::shared_ptr