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
parent
cf4defcd47
commit
5ca6d04e02
|
@ -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)
|
||||
|
|
32
README.md
32
README.md
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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><!--
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
|
@ -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__)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue