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() 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 # 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 # 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 # Homebrew or macOS system libraries into the .app file is highly likely to result
@ -307,6 +307,8 @@ if(ENABLE_GUI)
elseif(APPLE) elseif(APPLE)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_library(APPKIT_LIBRARY AppKit REQUIRED) find_library(APPKIT_LIBRARY AppKit REQUIRED)
elseif(EMSCRIPTEN)
# Everything is built in
else() else()
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(SpaceWare) 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. 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 ## Building on macOS
You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed 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

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /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") 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_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif() 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. # First, set up registration functions for the kinds of resources we handle.
set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/) set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/)
set(resource_list) set(resource_list)
set(resource_names)
if(WIN32) if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in
${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc) ${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc)
@ -83,6 +84,23 @@ elseif(APPLE)
DEPENDS ${source} DEPENDS ${source}
VERBATIM) VERBATIM)
endfunction() 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 else() # Unix
include(GNUInstallDirs) include(GNUInstallDirs)
@ -112,6 +130,7 @@ function(add_resources)
foreach(name ${ARGN}) foreach(name ${ARGN})
add_resource(${name}) 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() endforeach()
endfunction() endfunction()
@ -306,4 +325,6 @@ add_custom_target(resources
DEPENDS ${resource_list}) DEPENDS ${resource_list})
if(WIN32) if(WIN32)
set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file}) set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file})
elseif(EMSCRIPTEN)
set_property(TARGET resources PROPERTY NAMES ${resource_names})
endif() endif()

View File

@ -103,7 +103,8 @@ endif()
set(every_platform_SOURCES set(every_platform_SOURCES
platform/guiwin.cpp platform/guiwin.cpp
platform/guigtk.cpp platform/guigtk.cpp
platform/guimac.mm) platform/guimac.mm
platform/guihtml.cpp)
# solvespace library # solvespace library
@ -324,6 +325,49 @@ if(ENABLE_GUI)
if(MSVC) if(MSVC)
set_target_properties(solvespace PROPERTIES set_target_properties(solvespace PROPERTIES
LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF") 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()
endif() endif()
@ -367,7 +411,7 @@ endif()
# solvespace unix package # solvespace unix package
if(NOT (WIN32 OR APPLE)) if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
if(ENABLE_GUI) if(ENABLE_GUI)
install(TARGETS solvespace install(TARGETS solvespace
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

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

View File

@ -221,6 +221,7 @@ public:
std::function<bool(KeyboardEvent)> onKeyboardEvent; std::function<bool(KeyboardEvent)> onKeyboardEvent;
std::function<void(std::string)> onEditingDone; std::function<void(std::string)> onEditingDone;
std::function<void(double)> onScrollbarAdjusted; std::function<void(double)> onScrollbarAdjusted;
std::function<void()> onContextLost;
std::function<void()> onRender; std::function<void()> onRender;
virtual ~Window() = default; 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; return path;
} }
#elif defined(EMSCRIPTEN)
static Platform::Path ResourcePath(const std::string &name) {
return Path::From("res/" + name);
}
#elif !defined(WIN32) #elif !defined(WIN32)
# if defined(__linux__) # if defined(__linux__)

View File

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