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()
|
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)
|
||||||
|
|
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.
|
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
|
||||||
|
|
|
@ -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)
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -111,7 +129,8 @@ endif()
|
||||||
function(add_resources)
|
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()
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
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__)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue