Add a very experimental Emscripten port.

Web: Emscripten port updated to current tools. Add saving of options in local storage.

U  Web: Emscripten port updated to current tools. Add saving of options in local storage.
pull/1285/head
whitequark 2018-07-18 22:08:28 +00:00 committed by ruevs
parent cf4defcd47
commit 5ca6d04e02
16 changed files with 1763 additions and 6 deletions

View File

@ -212,7 +212,7 @@ if(NOT EXISTS "${EIGEN3_INCLUDE_DIRS}")
endif()
if(WIN32 OR APPLE)
if(WIN32 OR APPLE OR EMSCRIPTEN)
# On Win32 and macOS we use vendored packages, since there is little to no benefit
# to trying to find system versions. In particular, trying to link to libraries from
# Homebrew or macOS system libraries into the .app file is highly likely to result
@ -307,6 +307,8 @@ if(ENABLE_GUI)
elseif(APPLE)
find_package(OpenGL REQUIRED)
find_library(APPKIT_LIBRARY AppKit REQUIRED)
elseif(EMSCRIPTEN)
# Everything is built in
else()
find_package(OpenGL REQUIRED)
find_package(SpaceWare)

View File

@ -168,6 +168,38 @@ command-line interface is built as `build/bin/solvespace-cli.exe`.
Space Navigator support will not be available.
## Building for web
You will need [Emscripten][]. First, install and prepare `emsdk`:
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install latest
./emsdk update latest
source ./emsdk_env.sh
cd ..
Before building, check out the project and the necessary submodules:
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update
After that, build SolveSpace as following:
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-emscripten.cmake \
-DCMAKE_BUILD_TYPE=Release
make
The graphical interface is built as multiple files in the `build/bin` directory with names
starting with `solvespace`. It can be run locally with `emrun build/bin/solvespace.html`.
The command-line interface is not available.
[emscripten]: https://kripken.github.io/emscripten-site/
## Building on macOS
You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed

View File

@ -0,0 +1,9 @@
set(EMSCRIPTEN 1)
set(CMAKE_C_OUTPUT_EXTENSION ".o")
set(CMAKE_CXX_OUTPUT_EXTENSION ".o")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
set(CMAKE_SIZEOF_VOID_P 4)
set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE)

View File

@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Emscripten)
set(TRIPLE asmjs-unknown-emscripten)
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)
set(M_LIBRARY m)

View File

@ -3,4 +3,8 @@ if(MSVC)
set(CMAKE_C_FLAGS_MINSIZEREL_INIT "/MT /O1 /Ob1 /D NDEBUG")
set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif()
endif()
if(EMSCRIPTEN)
set(CMAKE_C_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif()
if(EMSCRIPTEN)
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -1,6 +1,7 @@
# First, set up registration functions for the kinds of resources we handle.
set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/)
set(resource_list)
set(resource_names)
if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in
${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc)
@ -83,6 +84,23 @@ elseif(APPLE)
DEPENDS ${source}
VERBATIM)
endfunction()
elseif(EMSCRIPTEN)
set(resource_dir ${CMAKE_BINARY_DIR}/src/res)
function(add_resource name)
set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name})
set(target ${resource_dir}/${name})
set(resource_list "${resource_list};${target}" PARENT_SCOPE)
set(resource_names "${resource_names};res/${name}" PARENT_SCOPE)
add_custom_command(
OUTPUT ${target}
COMMAND ${CMAKE_COMMAND} -E make_directory ${resource_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target}
COMMENT "Copying resource ${name}"
DEPENDS ${source}
VERBATIM)
endfunction()
else() # Unix
include(GNUInstallDirs)
@ -111,7 +129,8 @@ endif()
function(add_resources)
foreach(name ${ARGN})
add_resource(${name})
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_names "${resource_names}" PARENT_SCOPE)
endforeach()
endfunction()
@ -306,4 +325,6 @@ add_custom_target(resources
DEPENDS ${resource_list})
if(WIN32)
set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file})
elseif(EMSCRIPTEN)
set_property(TARGET resources PROPERTY NAMES ${resource_names})
endif()

View File

@ -103,7 +103,8 @@ endif()
set(every_platform_SOURCES
platform/guiwin.cpp
platform/guigtk.cpp
platform/guimac.mm)
platform/guimac.mm
platform/guihtml.cpp)
# solvespace library
@ -324,6 +325,49 @@ if(ENABLE_GUI)
if(MSVC)
set_target_properties(solvespace PROPERTIES
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF")
elseif(APPLE)
set_target_properties(solvespace PROPERTIES
OUTPUT_NAME SolveSpace)
elseif(EMSCRIPTEN)
set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/emshell.html)
set(LINK_FLAGS
--bind --shell-file ${SHELL}
--no-heap-copy -s ALLOW_MEMORY_GROWTH=1
-s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728)
get_target_property(resource_names resources NAMES)
foreach(resource ${resource_names})
list(APPEND LINK_FLAGS --preload-file ${resource})
endforeach()
if(CMAKE_BUILD_TYPE STREQUAL Debug)
list(APPEND LINK_FLAGS
--emrun --emit-symbol-map
-s DEMANGLE_SUPPORT=1
-s SAFE_HEAP=1
-s WASM=1)
endif()
string(REPLACE ";" " " LINK_FLAGS "${LINK_FLAGS}")
set_target_properties(solvespace PROPERTIES
LINK_FLAGS "${LINK_FLAGS}")
set_source_files_properties(platform/guihtml.cpp PROPERTIES
OBJECT_DEPENDS ${SHELL})
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.css
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.css
COMMENT "Copying UI stylesheet"
VERBATIM)
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js
COMMENT "Copying UI script"
VERBATIM)
endif()
endif()
@ -367,7 +411,7 @@ endif()
# solvespace unix package
if(NOT (WIN32 OR APPLE))
if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
if(ENABLE_GUI)
install(TARGETS solvespace
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

@ -433,6 +433,11 @@ void GraphicsWindow::Init() {
// a canvas.
window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
window->onContextLost = [&] {
canvas = NULL;
persistentCanvas = NULL;
persistentDirty = true;
};
window->onRender = std::bind(&GraphicsWindow::Paint, this);
window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);

View File

@ -221,6 +221,7 @@ public:
std::function<bool(KeyboardEvent)> onKeyboardEvent;
std::function<void(std::string)> onEditingDone;
std::function<void(double)> onScrollbarAdjusted;
std::function<void()> onContextLost;
std::function<void()> onRender;
virtual ~Window() = default;

921
src/platform/guihtml.cpp Normal file
View File

@ -0,0 +1,921 @@
//-----------------------------------------------------------------------------
// The Emscripten-based implementation of platform-dependent GUI functionality.
//
// Copyright 2018 whitequark
//-----------------------------------------------------------------------------
#include <emscripten.h>
#include <emscripten/val.h>
#include <emscripten/html5.h>
#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(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<val>("intArrayFromString", emStr, true) ;
val::global("window").set("$Wrap$input", emArray);
char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, 'i8', ALLOC_NORMAL));
std::string str(strC, emArray["length"].as<int>());
free(strC);
return str;
}
static void CallStdFunction(void *data) {
std::function<void()> *func = (std::function<void()> *)data;
if(*func) {
(*func)();
}
}
static val Wrap(std::function<void()> *func) {
EM_ASM($Wrap$ret = Module.dynCall_vi.bind(null, $0, $1), CallStdFunction, func);
return val::global("window")["$Wrap$ret"];
}
//-----------------------------------------------------------------------------
// Fatal errors
//-----------------------------------------------------------------------------
void FatalError(std::string message) {
fprintf(stderr, "%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) {
// FIXME(emscripten): implement
}
uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) {
// FIXME(emscripten): implement
return defaultValue;
}
void FreezeFloat(const std::string &key, double value) {
// FIXME(emscripten): implement
}
double ThawFloat(const std::string &key, double defaultValue = 0.0) {
// FIXME(emscripten): implement
return defaultValue;
}
void FreezeString(const std::string &key, const std::string &value) {
// FIXME(emscripten): implement
}
std::string ThawString(const std::string &key,
const std::string &defaultValue = "") {
// FIXME(emscripten): implement
return defaultValue;
}
};
SettingsRef GetSettings() {
return std::make_shared<SettingsImplHtml>();
}
//-----------------------------------------------------------------------------
// 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<TimerImplHtml>(new TimerImplHtml);
}
//-----------------------------------------------------------------------------
// Menus
//-----------------------------------------------------------------------------
class MenuItemImplHtml : public MenuItem {
public:
val htmlMenuItem;
MenuItemImplHtml() :
htmlMenuItem(val::global("document").call<val>("createElement", val("li")))
{}
void SetAccelerator(KeyboardEvent accel) override {
val htmlAccel = htmlMenuItem.call<val>("querySelector", val(".accel"));
if(htmlAccel.as<bool>()) {
htmlAccel.call<void>("remove");
}
htmlAccel = val::global("document").call<val>("createElement", val("span"));
htmlAccel.call<void>("setAttribute", val("class"), val("accel"));
htmlAccel.set("innerText", AcceleratorDescription(accel));
htmlMenuItem.call<void>("appendChild", htmlAccel);
}
void SetIndicator(Indicator type) override {
val htmlClasses = htmlMenuItem["classList"];
htmlClasses.call<void>("remove", val("check"));
htmlClasses.call<void>("remove", val("radio"));
switch(type) {
case Indicator::NONE:
break;
case Indicator::CHECK_MARK:
htmlClasses.call<void>("add", val("check"));
break;
case Indicator::RADIO_MARK:
htmlClasses.call<void>("add", val("radio"));
break;
}
}
void SetEnabled(bool enabled) override {
if(enabled) {
htmlMenuItem["classList"].call<void>("remove", val("disabled"));
} else {
htmlMenuItem["classList"].call<void>("add", val("disabled"));
}
}
void SetActive(bool active) override {
if(active) {
htmlMenuItem["classList"].call<void>("add", val("active"));
} else {
htmlMenuItem["classList"].call<void>("remove", val("active"));
}
}
};
class MenuImplHtml;
static std::shared_ptr<MenuImplHtml> popupMenuOnScreen;
class MenuImplHtml : public Menu,
public std::enable_shared_from_this<MenuImplHtml> {
public:
val htmlMenu;
std::vector<std::shared_ptr<MenuItemImplHtml>> menuItems;
std::vector<std::shared_ptr<MenuImplHtml>> subMenus;
std::function<void()> popupDismissFunc;
MenuImplHtml() :
htmlMenu(val::global("document").call<val>("createElement", val("ul")))
{
htmlMenu["classList"].call<void>("add", val("menu"));
}
MenuItemRef AddItem(const std::string &label, std::function<void()> onTrigger,
bool mnemonics = true) override {
std::shared_ptr<MenuItemImplHtml> menuItem = std::make_shared<MenuItemImplHtml>();
menuItems.push_back(menuItem);
menuItem->onTrigger = onTrigger;
if(mnemonics) {
val::global("window").call<void>("setLabelWithMnemonic", menuItem->htmlMenuItem,
Wrap(label));
} else {
val htmlLabel = val::global("document").call<val>("createElement", val("span"));
htmlLabel["classList"].call<void>("add", val("label"));
htmlLabel["innerText"] = Wrap(label);
menuItem->htmlMenuItem.call<void>("appendChild", htmlLabel);
}
menuItem->htmlMenuItem.call<void>("addEventListener", val("trigger"),
Wrap(&menuItem->onTrigger));
htmlMenu.call<void>("appendChild", menuItem->htmlMenuItem);
return menuItem;
}
std::shared_ptr<Menu> AddSubMenu(const std::string &label) override {
val htmlMenuItem = val::global("document").call<val>("createElement", val("li"));
val::global("window").call<void>("setLabelWithMnemonic", htmlMenuItem, Wrap(label));
htmlMenuItem["classList"].call<void>("add", val("has-submenu"));
htmlMenu.call<void>("appendChild", htmlMenuItem);
std::shared_ptr<MenuImplHtml> subMenu = std::make_shared<MenuImplHtml>();
subMenus.push_back(subMenu);
htmlMenuItem.call<void>("appendChild", subMenu->htmlMenu);
return subMenu;
}
void AddSeparator() override {
val htmlSeparator = val::global("document").call<val>("createElement", val("li"));
htmlSeparator["classList"].call<void>("add", val("separator"));
htmlMenu.call<void>("appendChild", htmlSeparator);
}
void PopUp() override {
if(popupMenuOnScreen) {
popupMenuOnScreen->htmlMenu.call<void>("remove");
popupMenuOnScreen = NULL;
}
EmscriptenMouseEvent emStatus = {};
sscheck(emscripten_get_mouse_status(&emStatus));
htmlMenu["classList"].call<void>("add", val("popup"));
htmlMenu["style"].set("left", std::to_string(emStatus.clientX) + "px");
htmlMenu["style"].set("top", std::to_string(emStatus.clientY) + "px");
val::global("document")["body"].call<void>("appendChild", htmlMenu);
popupMenuOnScreen = shared_from_this();
}
void Clear() override {
while(htmlMenu["childElementCount"].as<int>() > 0) {
htmlMenu["firstChild"].call<void>("remove");
}
}
};
MenuRef CreateMenu() {
return std::make_shared<MenuImplHtml>();
}
class MenuBarImplHtml final : public MenuBar {
public:
val htmlMenuBar;
std::vector<std::shared_ptr<MenuImplHtml>> subMenus;
MenuBarImplHtml() :
htmlMenuBar(val::global("document").call<val>("createElement", val("ul")))
{
htmlMenuBar["classList"].call<void>("add", val("menu"));
htmlMenuBar["classList"].call<void>("add", val("menubar"));
}
std::shared_ptr<Menu> AddSubMenu(const std::string &label) override {
val htmlMenuItem = val::global("document").call<val>("createElement", val("li"));
val::global("window").call<void>("setLabelWithMnemonic", htmlMenuItem, Wrap(label));
htmlMenuBar.call<void>("appendChild", htmlMenuItem);
std::shared_ptr<MenuImplHtml> subMenu = std::make_shared<MenuImplHtml>();
subMenus.push_back(subMenu);
htmlMenuItem.call<void>("appendChild", subMenu->htmlMenu);
return subMenu;
}
void Clear() override {
while(htmlMenuBar["childElementCount"].as<int>() > 0) {
htmlMenuBar["firstChild"].call<void>("remove");
}
}
};
MenuBarRef GetOrCreateMainMenu(bool *unique) {
*unique = false;
return std::make_shared<MenuBarImplHtml>();
}
//-----------------------------------------------------------------------------
// Windows
//-----------------------------------------------------------------------------
static KeyboardEvent handledKeyboardEvent;
class WindowImplHtml final : public Window {
public:
std::string emCanvasId;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE emContext = 0;
val htmlContainer;
val htmlEditor;
std::function<void()> editingDoneFunc;
std::shared_ptr<MenuBarImplHtml> menuBar;
WindowImplHtml(val htmlContainer, std::string emCanvasId) :
emCanvasId(emCanvasId),
htmlContainer(htmlContainer),
htmlEditor(val::global("document").call<val>("createElement", val("input")))
{
htmlEditor["classList"].call<void>("add", val("editor"));
htmlEditor["style"].set("display", "none");
editingDoneFunc = [this] {
if(onEditingDone) {
onEditingDone(Unwrap(htmlEditor["value"]));
}
};
htmlEditor.call<void>("addEventListener", val("trigger"), Wrap(&editingDoneFunc));
htmlContainer.call<void>("appendChild", htmlEditor);
sscheck(emscripten_set_resize_callback(
"#window", this, /*useCapture=*/false,
WindowImplHtml::ResizeCallback));
sscheck(emscripten_set_resize_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::ResizeCallback));
sscheck(emscripten_set_mousemove_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_mousedown_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_click_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_dblclick_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_mouseup_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_mouseleave_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::MouseCallback));
sscheck(emscripten_set_wheel_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::WheelCallback));
sscheck(emscripten_set_keydown_callback(
"#window", this, /*useCapture=*/false,
WindowImplHtml::KeyboardCallback));
sscheck(emscripten_set_keyup_callback(
"#window", this, /*useCapture=*/false,
WindowImplHtml::KeyboardCallback));
sscheck(emscripten_set_webglcontextlost_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::ContextLostCallback));
sscheck(emscripten_set_webglcontextrestored_callback(
emCanvasId.c_str(), this, /*useCapture=*/false,
WindowImplHtml::ContextRestoredCallback));
ResizeCanvasElement();
SetupWebGLContext();
}
~WindowImplHtml() {
if(emContext != 0) {
sscheck(emscripten_webgl_destroy_context(emContext));
}
}
static EM_BOOL ResizeCallback(int emEventType, const EmscriptenUiEvent *emEvent, void *data) {
WindowImplHtml *window = (WindowImplHtml *)data;
window->Invalidate();
return EM_TRUE;
}
static EM_BOOL MouseCallback(int emEventType, const EmscriptenMouseEvent *emEvent,
void *data) {
if(val::global("window").call<bool>("isModal")) return EM_FALSE;
WindowImplHtml *window = (WindowImplHtml *)data;
MouseEvent event = {};
switch(emEventType) {
case EMSCRIPTEN_EVENT_MOUSEMOVE:
event.type = MouseEvent::Type::MOTION;
break;
case EMSCRIPTEN_EVENT_MOUSEDOWN:
event.type = MouseEvent::Type::PRESS;
break;
case EMSCRIPTEN_EVENT_DBLCLICK:
event.type = MouseEvent::Type::DBL_PRESS;
break;
case EMSCRIPTEN_EVENT_MOUSEUP:
event.type = MouseEvent::Type::RELEASE;
break;
case EMSCRIPTEN_EVENT_MOUSELEAVE:
event.type = MouseEvent::Type::LEAVE;
break;
default:
return EM_FALSE;
}
switch(emEventType) {
case EMSCRIPTEN_EVENT_MOUSEMOVE:
if(emEvent->buttons & 1) {
event.button = MouseEvent::Button::LEFT;
} else if(emEvent->buttons & 2) {
event.button = MouseEvent::Button::RIGHT;
} else if(emEvent->buttons & 4) {
event.button = MouseEvent::Button::MIDDLE;
}
break;
case EMSCRIPTEN_EVENT_MOUSEDOWN:
case EMSCRIPTEN_EVENT_DBLCLICK:
case EMSCRIPTEN_EVENT_MOUSEUP:
switch(emEvent->button) {
case 0: event.button = MouseEvent::Button::LEFT; break;
case 1: event.button = MouseEvent::Button::MIDDLE; break;
case 2: event.button = MouseEvent::Button::RIGHT; break;
}
break;
default:
return EM_FALSE;
}
event.x = emEvent->targetX;
event.y = emEvent->targetY;
event.shiftDown = emEvent->shiftKey || emEvent->altKey;
event.controlDown = emEvent->ctrlKey;
if(window->onMouseEvent) {
return window->onMouseEvent(event);
}
return EM_FALSE;
}
static EM_BOOL WheelCallback(int emEventType, const EmscriptenWheelEvent *emEvent,
void *data) {
if(val::global("window").call<bool>("isModal")) return EM_FALSE;
WindowImplHtml *window = (WindowImplHtml *)data;
MouseEvent event = {};
if(emEvent->deltaY != 0) {
event.type = MouseEvent::Type::SCROLL_VERT;
event.scrollDelta = -emEvent->deltaY * 0.1;
} else {
return EM_FALSE;
}
EmscriptenMouseEvent emStatus = {};
sscheck(emscripten_get_mouse_status(&emStatus));
event.x = emStatus.targetX;
event.y = emStatus.targetY;
event.shiftDown = emStatus.shiftKey;
event.controlDown = emStatus.ctrlKey;
if(window->onMouseEvent) {
return window->onMouseEvent(event);
}
return EM_FALSE;
}
static EM_BOOL KeyboardCallback(int emEventType, const EmscriptenKeyboardEvent *emEvent,
void *data) {
if(emEvent->altKey) return EM_FALSE;
if(emEvent->repeat) return EM_FALSE;
WindowImplHtml *window = (WindowImplHtml *)data;
KeyboardEvent event = {};
switch(emEventType) {
case EMSCRIPTEN_EVENT_KEYDOWN:
event.type = KeyboardEvent::Type::PRESS;
break;
case EMSCRIPTEN_EVENT_KEYUP:
event.type = KeyboardEvent::Type::RELEASE;
break;
default:
return EM_FALSE;
}
event.shiftDown = emEvent->shiftKey;
event.controlDown = emEvent->ctrlKey;
std::string key = emEvent->key;
if(key[0] == 'F' && isdigit(key[1])) {
event.key = KeyboardEvent::Key::FUNCTION;
event.num = std::stol(key.substr(1));
} else {
event.key = KeyboardEvent::Key::CHARACTER;
auto utf8 = ReadUTF8(key);
if(++utf8.begin() == utf8.end()) {
event.chr = tolower(*utf8.begin());
} else if(key == "Escape") {
event.chr = '\e';
} else if(key == "Tab") {
event.chr = '\t';
} else if(key == "Backspace") {
event.chr = '\b';
} else if(key == "Delete") {
event.chr = '\x7f';
} else {
return EM_FALSE;
}
if(event.chr == '>' && event.shiftDown) {
event.shiftDown = false;
}
}
if(event.Equals(handledKeyboardEvent)) return EM_FALSE;
if(val::global("window").call<bool>("isModal")) {
handledKeyboardEvent = {};
return EM_FALSE;
}
if(window->onKeyboardEvent) {
if(window->onKeyboardEvent(event)) {
handledKeyboardEvent = event;
return EM_TRUE;
}
}
return EM_FALSE;
}
void SetupWebGLContext() {
EmscriptenWebGLContextAttributes emAttribs = {};
emscripten_webgl_init_context_attributes(&emAttribs);
emAttribs.alpha = false;
emAttribs.failIfMajorPerformanceCaveat = true;
sscheck(emContext = emscripten_webgl_create_context(emCanvasId.c_str(), &emAttribs));
dbp("Canvas %s: got context %d", emCanvasId.c_str(), emContext);
}
static int ContextLostCallback(int eventType, const void *reserved, void *data) {
WindowImplHtml *window = (WindowImplHtml *)data;
dbp("Canvas %s: context lost", window->emCanvasId.c_str());
window->emContext = 0;
if(window->onContextLost) {
window->onContextLost();
}
return EM_TRUE;
}
static int ContextRestoredCallback(int eventType, const void *reserved, void *data) {
WindowImplHtml *window = (WindowImplHtml *)data;
dbp("Canvas %s: context restored", window->emCanvasId.c_str());
window->SetupWebGLContext();
return EM_TRUE;
}
void ResizeCanvasElement() {
double width, height;
std::string htmlContainerId = htmlContainer["id"].as<std::string>();
sscheck(emscripten_get_element_css_size(htmlContainerId.c_str(), &width, &height));
width *= emscripten_get_device_pixel_ratio();
height *= emscripten_get_device_pixel_ratio();
int curWidth, curHeight;
sscheck(emscripten_get_canvas_element_size(emCanvasId.c_str(), &curWidth, &curHeight));
if(curWidth != (int)width || curHeight != (int)curHeight) {
dbp("Canvas %s: resizing to (%g,%g)", emCanvasId.c_str(), width, height);
sscheck(emscripten_set_canvas_element_size(
emCanvasId.c_str(), (int)width, (int)height));
}
}
static void RenderCallback(void *data) {
WindowImplHtml *window = (WindowImplHtml *)data;
if(window->emContext == 0) {
dbp("Canvas %s: cannot render: no context", window->emCanvasId.c_str());
return;
}
window->ResizeCanvasElement();
sscheck(emscripten_webgl_make_context_current(window->emContext));
if(window->onRender) {
window->onRender();
}
}
double GetPixelDensity() override {
return 96.0 * emscripten_get_device_pixel_ratio();
}
int GetDevicePixelRatio() override {
return (int)emscripten_get_device_pixel_ratio();
}
bool IsVisible() override {
// FIXME(emscripten): implement
return true;
}
void SetVisible(bool visible) override {
// FIXME(emscripten): implement
}
void Focus() override {
// Do nothing, we can't affect focus of browser windows.
}
bool IsFullScreen() override {
EmscriptenFullscreenChangeEvent emEvent = {};
sscheck(emscripten_get_fullscreen_status(&emEvent));
return emEvent.isFullscreen;
}
void SetFullScreen(bool fullScreen) override {
if(fullScreen) {
EmscriptenFullscreenStrategy emStrategy = {};
emStrategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
emStrategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF;
emStrategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
sscheck(emscripten_request_fullscreen_strategy(
emCanvasId.c_str(), /*deferUntilInEventHandler=*/true, &emStrategy));
} else {
sscheck(emscripten_exit_fullscreen());
}
}
void SetTitle(const std::string &title) override {
// FIXME(emscripten): implement
}
void SetMenuBar(MenuBarRef menuBar) override {
std::shared_ptr<MenuBarImplHtml> menuBarImpl =
std::static_pointer_cast<MenuBarImplHtml>(menuBar);
this->menuBar = menuBarImpl;
val htmlBody = val::global("document")["body"];
val htmlCurrentMenuBar = htmlBody.call<val>("querySelector", val(".menubar"));
if(htmlCurrentMenuBar.as<bool>()) {
htmlCurrentMenuBar.call<void>("remove");
}
htmlBody.call<void>("insertBefore", menuBarImpl->htmlMenuBar,
htmlBody["firstChild"]);
ResizeCanvasElement();
}
void GetContentSize(double *width, double *height) override {
sscheck(emscripten_get_element_css_size(emCanvasId.c_str(), width, height));
}
void SetMinContentSize(double width, double height) override {
// Do nothing, we can't affect sizing of browser windows.
}
void FreezePosition(SettingsRef settings, const std::string &key) override {
// Do nothing, we can't position browser windows.
}
void ThawPosition(SettingsRef settings, const std::string &key) override {
// Do nothing, we can't position browser windows.
}
void SetCursor(Cursor cursor) override {
std::string htmlCursor;
switch(cursor) {
case Cursor::POINTER: htmlCursor = "default"; break;
case Cursor::HAND: htmlCursor = "pointer"; break;
}
htmlContainer["style"].set("cursor", htmlCursor);
}
void SetTooltip(const std::string &text) override {
// FIXME(emscripten): implement
}
bool IsEditorVisible() override {
return htmlEditor["style"]["display"].as<std::string>() != "none";
}
void ShowEditor(double x, double y, double fontHeight, double minWidth,
bool isMonospace, const std::string &text) override {
htmlEditor["style"].set("display", val(""));
htmlEditor["style"].set("left", std::to_string(x - 4) + "px");
htmlEditor["style"].set("top", std::to_string(y - fontHeight - 2) + "px");
htmlEditor["style"].set("fontSize", std::to_string(fontHeight) + "px");
htmlEditor["style"].set("minWidth", std::to_string(minWidth) + "px");
htmlEditor["style"].set("fontFamily", isMonospace ? "monospace" : "sans");
htmlEditor.set("value", Wrap(text));
htmlEditor.call<void>("focus");
}
void HideEditor() override {
htmlEditor["style"].set("display", val("none"));
}
void SetScrollbarVisible(bool visible) override {
// FIXME(emscripten): implement
}
double scrollbarPos = 0.0;
void ConfigureScrollbar(double min, double max, double pageSize) override {
// FIXME(emscripten): implement
}
double GetScrollbarPosition() override {
// FIXME(emscripten): implement
return scrollbarPos;
}
void SetScrollbarPosition(double pos) override {
// FIXME(emscripten): implement
scrollbarPos = pos;
}
void Invalidate() override {
emscripten_async_call(WindowImplHtml::RenderCallback, this, -1);
}
};
WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
static int windowNum;
std::string htmlContainerId = std::string("container") + std::to_string(windowNum);
val htmlContainer =
val::global("document").call<val>("getElementById", htmlContainerId);
std::string emCanvasId = std::string("canvas") + std::to_string(windowNum);
windowNum++;
return std::make_shared<WindowImplHtml>(htmlContainer, emCanvasId);
}
//-----------------------------------------------------------------------------
// 3DConnexion support
//-----------------------------------------------------------------------------
void Open3DConnexion() {}
void Close3DConnexion() {}
void Request3DConnexionEventsForWindow(WindowRef window) {}
//-----------------------------------------------------------------------------
// Message dialogs
//-----------------------------------------------------------------------------
class MessageDialogImplHtml;
static std::vector<std::shared_ptr<MessageDialogImplHtml>> dialogsOnScreen;
class MessageDialogImplHtml final : public MessageDialog,
public std::enable_shared_from_this<MessageDialogImplHtml> {
public:
val htmlModal;
val htmlDialog;
val htmlMessage;
val htmlDescription;
val htmlButtons;
std::vector<std::function<void()>> responseFuncs;
MessageDialogImplHtml() :
htmlModal(val::global("document").call<val>("createElement", val("div"))),
htmlDialog(val::global("document").call<val>("createElement", val("div"))),
htmlMessage(val::global("document").call<val>("createElement", val("strong"))),
htmlDescription(val::global("document").call<val>("createElement", val("p"))),
htmlButtons(val::global("document").call<val>("createElement", val("div")))
{
htmlModal["classList"].call<void>("add", val("modal"));
htmlModal.call<void>("appendChild", htmlDialog);
htmlDialog["classList"].call<void>("add", val("dialog"));
htmlDialog.call<void>("appendChild", htmlMessage);
htmlDialog.call<void>("appendChild", htmlDescription);
htmlButtons["classList"].call<void>("add", val("buttons"));
htmlDialog.call<void>("appendChild", htmlButtons);
}
void SetType(Type type) {
// FIXME(emscripten): implement
}
void SetTitle(std::string title) {
// FIXME(emscripten): implement
}
void SetMessage(std::string message) {
htmlMessage.set("innerText", Wrap(message));
}
void SetDescription(std::string description) {
htmlDescription.set("innerText", Wrap(description));
}
void AddButton(std::string label, Response response, bool isDefault = false) {
val htmlButton = val::global("document").call<val>("createElement", val("div"));
htmlButton["classList"].call<void>("add", val("button"));
val::global("window").call<void>("setLabelWithMnemonic", htmlButton, Wrap(label));
if(isDefault) {
htmlButton["classList"].call<void>("add", val("selected"));
}
std::function<void()> responseFunc = [this, response] {
htmlModal.call<void>("remove");
if(onResponse) {
onResponse(response);
}
auto it = std::remove(dialogsOnScreen.begin(), dialogsOnScreen.end(),
shared_from_this());
dialogsOnScreen.erase(it);
};
responseFuncs.push_back(responseFunc);
htmlButton.call<void>("addEventListener", val("trigger"), Wrap(&responseFuncs.back()));
htmlButtons.call<void>("appendChild", htmlButton);
}
Response RunModal() {
ssassert(false, "RunModal not supported on Emscripten");
}
void ShowModal() {
dialogsOnScreen.push_back(shared_from_this());
val::global("document")["body"].call<void>("appendChild", htmlModal);
}
};
MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
return std::make_shared<MessageDialogImplHtml>();
}
//-----------------------------------------------------------------------------
// File dialogs
//-----------------------------------------------------------------------------
class FileDialogImplHtml : public FileDialog {
public:
// FIXME(emscripten): implement
};
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
// FIXME(emscripten): implement
return std::shared_ptr<FileDialogImplHtml>();
}
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
// FIXME(emscripten): implement
return std::shared_ptr<FileDialogImplHtml>();
}
//-----------------------------------------------------------------------------
// Application-wide APIs
//-----------------------------------------------------------------------------
std::vector<Platform::Path> GetFontFiles() {
return {};
}
void OpenInBrowser(const std::string &url) {
val::global("window").call<void>("open", Wrap(url));
}
void InitGui(int argc, char **argv) {
// FIXME(emscripten): get locale from user preferences
SetLocale("en_US");
}
static void MainLoopIteration() {
// We don't do anything here, as all our work is registered via timers.
}
void RunGui() {
emscripten_set_main_loop(MainLoopIteration, 0, /*simulate_infinite_loop=*/true);
}
void ExitGui() {
exit(0);
}
}
}

View File

@ -0,0 +1,82 @@
<!doctype html>
<html><!--
--><head><!--
--><meta charset="utf-8"><!--
--><title>SolveSpace Web Edition (EXPERIMENTAL)</title><!--
--><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.1/css/all.css" integrity="sha384-O8whS3fhG2OnA5Kas0Y9l3cfpmYjapjI0E4theH4iuMD+pLhbf6JI0jIMfYcK3yZ" crossorigin="anonymous"><!--
--><link rel="stylesheet" href="solvespaceui.css"><!--
--><script src="solvespaceui.js"></script><!--
--></head><!--
--><body><!--
--><div id="splash">
<div class="center">
<div id="spinner"></div>
<div id="status">Downloading...</div>
<div id="crash" style="display:none;">
SolveSpace has crashed. See console for details.<br>
The Web Edition of SolveSpace is experimental,<br>
and may not be as reliable as the Desktop Edition.<br>
<a href="javascript:window.location.reload()">Restart</a>
</div>
<progress id="progress" value="0" max="100" hidden="1"></progress>
</div>
</div><!--
FIXME(emscripten): without this, a window resize is required in Chrome
to get the layout to update and canvas size to match up. What?
--><ul class="menu menubar" style="visibility: hidden"><li>None</li></ul><!--
--><div id="container"><!--
--><div id="container0"><canvas id="canvas0"></canvas></div><!--
--><div id="container1"><canvas id="canvas1"></canvas></div><!--
--></div><!--
--><script type="text/javascript">
var splashElement = document.getElementById('splash');
var spinnerElement = document.getElementById('spinner');
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var crashElement = document.getElementById('crash');
var canvas0Element = document.getElementById('canvas0');
var canvas1Element = document.getElementById('canvas1');
canvas0Element.oncontextmenu = function(event) { event.preventDefault(); }
canvas1Element.oncontextmenu = function(event) { event.preventDefault(); }
var Module = {
preRun: [],
postRun: [],
print: console.log,
printErr: console.error,
state: 'loading',
setStatus: function(text) {
if(this.state == 'crashed') {
spinnerElement.style.display = 'none';
statusElement.style.display = 'none';
crashElement.style.display = '';
splashElement.style.display = '';
} else if(text != '') {
console.log('Status:', text);
statusElement.innerText = text;
} else if(this.state != 'done') {
console.log('Status: Done!');
splashElement.style.display = 'none';
this.state = 'done';
}
},
totalDependencies: 0,
monitorRunDependencies: function(remainingDependencies) {
this.totalDependencies = Math.max(this.totalDependencies, remainingDependencies);
if(remainingDependencies > 0) {
var completeDependencies = this.totalDependencies - remainingDependencies;
Module.setStatus('Preparing... (' + completeDependencies + '/' +
this.totalDependencies + ')');
}
}
};
Module.setStatus('Downloading...');
window.onerror = function() {
Module.state = 'crashed';
Module.setStatus();
return false;
};
</script><!--
-->{{{ SCRIPT }}}<!--
--></body></html><!--

View File

@ -0,0 +1,272 @@
* {
font-family: sans;
}
html, body {
padding: 0;
margin: 0;
background: black;
display: flex;
flex-direction: column;
}
html, body, canvas, #splash, #container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
overflow: hidden;
}
/* Splashscreen */
#splash {
z-index: 1000;
background: black;
color: white;
position: absolute;
}
#splash .center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
#splash a {
color: white;
}
#spinner {
height: 30px;
width: 30px;
margin: 0px auto;
border-left: 10px solid rgb(255, 255, 255);
border-top: 10px solid rgb(0, 255, 0);
border-right: 10px solid rgb(255, 0, 255);
border-bottom: 10px solid rgb(0, 255, 0);
border-radius: 100%;
animation: rotation 3s linear infinite;
margin-bottom: 5px;
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Buttons */
.button {
border: 1px solid hsl(0, 0%, 60%);
background: hsl(0, 0%, 10%);
color: white;
padding: 4px 8px;
cursor: default;
}
.button.selected {
background: hsl(0, 0%, 20%);
}
.button:hover {
background: hsl(0, 0%, 40%);
}
/* Editors */
.editor {
position: absolute;
padding: 1px 0;
border: none;
}
/* Menus */
.menu {
font-size: 0;
margin: 0;
padding: 0;
padding-right: 10px;
list-style-type: none;
background: hsl(0, 0%, 20%);
color: white;
cursor: default;
}
/* Normal menu items */
.menu > li {
z-index: 100;
font-size: 16px;
display: inline-flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
position: relative;
width: 100%;
height: 19px;
margin: 2px;
padding: 3px;
}
.menu > li::before, .menu > li::after {
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 12px;
}
.menu > li.hover,
.menu > li.selected,
.menu.menubar > li:hover:not(.selected) {
background: hsl(0, 0%, 30%);
}
.menu > li.disabled {
color: hsl(0, 0%, 30%);
}
/* Check and radio menu items */
.menu > li {
padding-left: 24px;
}
.menu > li::before {
position: absolute;
text-align: center;
left: 0px;
width: 24px;
}
.menu > li.check::before {
content: '\f0c8';
}
.menu > li.check.active::before {
content: '\f14a';
}
.menu > li.radio::before {
content: '\f111';
}
.menu > li.radio.active::before {
content: '\f192';
}
/* Separator menu items */
.menu > li.separator {
height: 0px;
border-top: 1px solid hsl(0, 0%, 30%);
margin: 0 2px 0 2px;
padding-top: 0;
padding-bottom: 0;
}
/* Accelerators */
.menu > li > .accel {
text-align: right;
margin-left: 20px;
}
/* Submenus */
.menu > li > .menu,
.menu.popup {
display: none;
white-space: normal;
padding-right: 31px;
}
.menu > li.has-submenu::after {
content: '\f0da';
}
.menu > li.selected > .menu,
.menu > li.hover > .menu,
.menu.popup {
display: block;
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
position: absolute;
left: 100%;
top: -3px;
}
/* Popup menus */
.menu.popup {
display: block;
position: absolute;
width: min-content;
}
/* Menubars */
.menubar {
padding-left: 5px;
}
.menubar > li {
width: auto;
width: fit-content;
margin: 0;
padding: 5px;
}
.menubar > li.selected {
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
padding: 4px;
}
.menubar.menu > li.selected > .menu {
display: block;
position: absolute;
left: -1px;
top: 27px;
}
/* Modal popups */
.modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: hsla(0, 0%, 0%, 60%);
}
.modal > div {
position: absolute;
top: 15%;
left: 50%;
transform: translate(-50%, 0%);
}
/* Dialogs */
.dialog {
border: 1px solid hsl(0, 0%, 30%);
background: hsl(0, 0%, 10%);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
min-width: 200px;
max-width: 400px;
white-space: pre-wrap;
}
.dialog > .buttons {
display: flex;
justify-content: space-around;
}
/* Mnemonics */
.label > u {
position: relative;
top: 0px;
text-decoration: none;
}
body.mnemonic .label > u {
border-bottom: 1px solid;
}
/* Canvases */
canvas {
border: 0px none;
background-color: black;
}
#container {
display: flex;
height: 100%;
}
/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */
#container0 {
flex-basis: 80%;
height: 100%;
position: relative;
}
#container1 {
flex-basis: 20%;
height: 100%;
position: relative;
}
#canvas1 {
min-width: 400px;
}

View File

@ -0,0 +1,346 @@
function isModal() {
var hasModal = !!document.querySelector('.modal');
var hasMenuBar = !!document.querySelector('.menubar .selected');
var hasPopupMenu = !!document.querySelector('.menu.popup');
var hasEditor = false;
document.querySelectorAll('.editor').forEach(function(editor) {
if(editor.style.display == "") {
hasEditor = true;
}
});
return hasModal || hasMenuBar || hasPopupMenu || hasEditor;
}
/* CSS helpers */
function hasClass(element, className) {
return element.classList.contains(className);
}
function addClass(element, className) {
element.classList.add(className);
}
function removeClass(element, className) {
element.classList.remove(className);
}
function removeClassFromAllChildren(element, className) {
element.querySelectorAll('.' + className).forEach(function(element) {
removeClass(element, className);
})
}
/* Mnemonic helpers */
function setLabelWithMnemonic(element, labelText) {
var label = document.createElement('span');
addClass(label, 'label');
element.appendChild(label);
var matches = labelText.match('(.*?)&(.)(.*)?');
if(matches) {
label.appendChild(document.createTextNode(matches[1]));
if(matches[2]) {
var mnemonic = document.createElement('u');
mnemonic.innerText = matches[2];
label.appendChild(mnemonic);
addClass(element, 'mnemonic-Key' + matches[2].toUpperCase());
}
if(matches[3]) {
label.appendChild(document.createTextNode(matches[3]));
}
} else {
label.appendChild(document.createTextNode(labelText))
}
}
/* Button helpers */
function isButton(element) {
return hasClass(element, 'button');
}
/* Button DOM traversal helpers */
function getButton(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getButton(element.parentElement);
} else if(isButton(element)) {
return element;
}
}
/* Button behavior */
window.addEventListener('click', function(event) {
var button = getButton(event.target);
if(button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener('keydown', function(event) {
var selected = document.querySelector('.button.selected');
if(!selected) return;
var outSelected, newSelected;
if(event.key == 'ArrowRight') {
outSelected = selected;
newSelected = selected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(event.key == 'ArrowLeft') {
outSelected = selected;
newSelected = selected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} else if(event.key == 'Enter') {
selected.dispatchEvent(new Event('trigger'));
}
if(outSelected) removeClass(outSelected, 'selected');
if(newSelected) addClass(newSelected, 'selected');
event.stopPropagation();
});
/* Editor helpers */
function isEditor(element) {
return hasClass(element, 'editor');
}
/* Editor DOM traversal helpers */
function getEditor(element) {
if(!element) return;
if(isEditor(element)) {
return element;
}
}
/* Editor behavior */
window.addEventListener('keydown', function(event) {
var editor = getEditor(event.target);
if(editor) {
if(event.key == 'Enter') {
editor.dispatchEvent(new Event('trigger'));
} else if(event.key == 'Escape') {
editor.style.display = 'none';
}
event.stopPropagation();
}
});
/* Menu helpers */
function isMenubar(element) {
return hasClass(element, 'menubar');
}
function isMenu(element) {
return hasClass(element, 'menu');
}
function isPopupMenu(element) {
return isMenu(element) && hasClass(element, 'popup')
}
function hasSubmenu(menuItem) {
return !!menuItem.querySelector('.menu');
}
/* Menu item helpers */
function isMenuItemSelectable(menuItem) {
return !(hasClass(menuItem, 'disabled') || hasClass(menuItem, 'separator'));
}
function isMenuItemSelected(menuItem) {
return hasClass(menuItem, 'selected') || hasClass(menuItem, 'hover');
}
function deselectMenuItem(menuItem) {
removeClass(menuItem, 'selected');
removeClass(menuItem, 'hover');
removeClassFromAllChildren(menuItem, 'selected');
removeClassFromAllChildren(menuItem, 'hover');
}
function selectMenuItem(menuItem) {
var menu = menuItem.parentElement;
removeClassFromAllChildren(menu, 'selected');
removeClassFromAllChildren(menu, 'hover');
if(isMenubar(menu)) {
addClass(menuItem, 'selected');
} else {
addClass(menuItem, 'hover');
}
}
function triggerMenuItem(menuItem) {
selectMenuItem(menuItem);
if(hasSubmenu(menuItem)) {
selectMenuItem(menuItem.querySelector('li:first-child'));
} else {
var parent = menuItem.parentElement;
while(!isMenubar(parent) && !isPopupMenu(parent)) {
parent = parent.parentElement;
}
removeClassFromAllChildren(parent, 'selected');
removeClassFromAllChildren(parent, 'hover');
if(isPopupMenu(parent)) {
parent.remove();
}
menuItem.dispatchEvent(new Event('trigger'));
}
}
/* Menu DOM traversal helpers */
function getMenuItem(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getMenuItem(element.parentElement);
} else if(element.tagName == 'LI' && isMenu(element.parentElement)) {
return element;
}
}
function getMenu(element) {
if(!element) return;
if(isMenu(element)) {
return element;
} else {
var menuItem = getMenuItem(element);
if(menuItem && isMenu(menuItem.parentElement)) {
return menuItem.parentElement;
}
}
}
/* Menu behavior */
window.addEventListener('click', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener('mouseover', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu) {
var selected = menu.querySelectorAll('.selected, .hover');
if(isMenubar(menu)) {
if(selected.length > 0) {
selected.forEach(function(menuItem) {
if(selected != menuItem) {
deselectMenuItem(menuItem);
}
});
addClass(menuItem, 'selected');
}
} else {
if(isMenuItemSelectable(menuItem)) {
selectMenuItem(menuItem);
}
}
}
});
window.addEventListener('keydown', function(event) {
var allSelected = document.querySelectorAll('.menubar .selected, .menubar .hover,' +
'.menu.popup .selected, .menu.popup .hover');
if(allSelected.length == 0) return;
var selected = allSelected[allSelected.length - 1];
var outSelected, newSelected;
var isMenubarItem = isMenubar(getMenu(selected));
if(isMenubarItem && event.key == 'ArrowRight' ||
!isMenubarItem && event.key == 'ArrowDown') {
outSelected = selected;
newSelected = selected.nextElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.nextElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(isMenubarItem && event.key == 'ArrowLeft' ||
!isMenubarItem && event.key == 'ArrowUp') {
outSelected = selected;
newSelected = selected.previousElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.previousElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} else if(!isMenubarItem && event.key == 'ArrowRight') {
if(hasSubmenu(selected)) {
selectMenuItem(selected.querySelector('li:first-child'));
} else {
outSelected = allSelected[0];
newSelected = outSelected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
}
} else if(!isMenubarItem && event.key == 'ArrowLeft') {
if(allSelected.length > 2) {
outSelected = selected;
} else {
outSelected = allSelected[0];
newSelected = outSelected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
}
} else if(isMenubarItem && event.key == 'ArrowDown') {
newSelected = selected.querySelector('li:first-child');
} else if(event.key == 'Enter') {
triggerMenuItem(selected);
} else if(event.key == 'Escape') {
outSelected = allSelected[0];
} else {
var withMnemonic = getMenu(selected).querySelector('.mnemonic-' + event.key);
if(withMnemonic) {
triggerMenuItem(withMnemonic);
}
}
if(outSelected) deselectMenuItem(outSelected);
if(newSelected) selectMenuItem(newSelected);
event.stopPropagation();
});
/* Mnemonic behavior */
window.addEventListener('keydown', function(event) {
var withMnemonic;
if(event.altKey && event.key == 'Alt') {
addClass(document.body, 'mnemonic');
} else if(!isModal() && event.altKey && (withMnemonic =
document.querySelector('.menubar > .mnemonic-' + event.code))) {
triggerMenuItem(withMnemonic);
event.stopPropagation();
} else {
removeClass(document.body, 'mnemonic');
}
});
window.addEventListener('keyup', function(event) {
if(event.key == 'Alt') {
removeClass(document.body, 'mnemonic');
}
});

View File

@ -516,6 +516,12 @@ static Platform::Path ResourcePath(const std::string &name) {
return path;
}
#elif defined(EMSCRIPTEN)
static Platform::Path ResourcePath(const std::string &name) {
return Path::From("res/" + name);
}
#elif !defined(WIN32)
# if defined(__linux__)

View File

@ -6,7 +6,7 @@
#ifndef SOLVESPACE_GL3SHADER_H
#define SOLVESPACE_GL3SHADER_H
#if defined(WIN32)
#if defined(WIN32) || defined(EMSCRIPTEN)
# define GL_APICALL /*static linkage*/
# define GL_GLEXT_PROTOTYPES
# include <GLES2/gl2.h>