diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dd29d06..1f9cf678 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 8e444fdd..eedc3735 100644 --- a/README.md +++ b/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 diff --git a/cmake/Platform/Emscripten.cmake b/cmake/Platform/Emscripten.cmake new file mode 100644 index 00000000..73cc0d2b --- /dev/null +++ b/cmake/Platform/Emscripten.cmake @@ -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) diff --git a/cmake/Toolchain-emscripten.cmake b/cmake/Toolchain-emscripten.cmake new file mode 100644 index 00000000..ad20ea16 --- /dev/null +++ b/cmake/Toolchain-emscripten.cmake @@ -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) diff --git a/cmake/c_flag_overrides.cmake b/cmake/c_flag_overrides.cmake index b21f00e3..978b6b65 100644 --- a/cmake/c_flag_overrides.cmake +++ b/cmake/c_flag_overrides.cmake @@ -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() \ No newline at end of file +endif() + +if(EMSCRIPTEN) + set(CMAKE_C_FLAGS_DEBUG_INIT "-g4") +endif() diff --git a/cmake/cxx_flag_overrides.cmake b/cmake/cxx_flag_overrides.cmake index 67e00433..9c8d15fe 100644 --- a/cmake/cxx_flag_overrides.cmake +++ b/cmake/cxx_flag_overrides.cmake @@ -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() diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index 5ad3cf01..da12ec12 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -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() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 41c40d03..aa31d825 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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}) diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index 98ed54d6..28b7c4e1 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -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); diff --git a/src/platform/gui.h b/src/platform/gui.h index f63fa80b..8ee68710 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -221,6 +221,7 @@ public: std::function onKeyboardEvent; std::function onEditingDone; std::function onScrollbarAdjusted; + std::function onContextLost; std::function onRender; virtual ~Window() = default; diff --git a/src/platform/guihtml.cpp b/src/platform/guihtml.cpp new file mode 100644 index 00000000..c1547462 --- /dev/null +++ b/src/platform/guihtml.cpp @@ -0,0 +1,921 @@ +//----------------------------------------------------------------------------- +// The Emscripten-based implementation of platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include +#include +#include +#include "config.h" +#include "solvespace.h" + +using namespace emscripten; + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Emscripten API bridging +//----------------------------------------------------------------------------- + +#define sscheck(expr) do { \ + EMSCRIPTEN_RESULT emResult = (EMSCRIPTEN_RESULT)(expr); \ + if(emResult < 0) \ + HandleError(__FILE__, __LINE__, __func__, #expr, emResult); \ + } while(0) + +static void HandleError(const char *file, int line, const char *function, const char *expr, + EMSCRIPTEN_RESULT emResult) { + const char *error = "Unknown error"; + switch(emResult) { + case EMSCRIPTEN_RESULT_DEFERRED: error = "Deferred"; break; + case EMSCRIPTEN_RESULT_NOT_SUPPORTED: error = "Not supported"; break; + case EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED: error = "Failed (not deferred)"; break; + case EMSCRIPTEN_RESULT_INVALID_TARGET: error = "Invalid target"; break; + case EMSCRIPTEN_RESULT_UNKNOWN_TARGET: error = "Unknown target"; break; + case EMSCRIPTEN_RESULT_INVALID_PARAM: error = "Invalid parameter"; break; + case EMSCRIPTEN_RESULT_FAILED: error = "Failed"; break; + case EMSCRIPTEN_RESULT_NO_DATA: error = "No data"; break; + } + + std::string message; + message += ssprintf("File %s, line %u, function %s:\n", file, line, function); + message += ssprintf("Emscripten API call failed: %s.\n", expr); + message += ssprintf("Error: %s\n", error); + FatalError(message); +} + +static val Wrap(std::string str) { + // FIXME(emscripten): a nicer way to do this? + EM_ASM($Wrap$ret = UTF8ToString($0), str.c_str()); + return val::global("window")["$Wrap$ret"]; +} + +static std::string Unwrap(val emStr) { + // FIXME(emscripten): a nicer way to do this? + val emArray = val::global("window").call("intArrayFromString", emStr, true) ; + val::global("window").set("$Wrap$input", emArray); + char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, 'i8', ALLOC_NORMAL)); + std::string str(strC, emArray["length"].as()); + free(strC); + return str; +} + +static void CallStdFunction(void *data) { + std::function *func = (std::function *)data; + if(*func) { + (*func)(); + } +} + +static val Wrap(std::function *func) { + EM_ASM($Wrap$ret = Module.dynCall_vi.bind(null, $0, $1), CallStdFunction, func); + return val::global("window")["$Wrap$ret"]; +} + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(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(); +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplHtml : public Timer { +public: + static void Callback(void *arg) { + TimerImplHtml *timer = (TimerImplHtml *)arg; + if(timer->onTimeout) { + timer->onTimeout(); + } + } + + void RunAfter(unsigned milliseconds) override { + emscripten_async_call(TimerImplHtml::Callback, this, milliseconds + 1); + } + + void RunAfterNextFrame() override { + emscripten_async_call(TimerImplHtml::Callback, this, 0); + } + + void RunAfterProcessingEvents() override { + emscripten_push_uncounted_main_loop_blocker(TimerImplHtml::Callback, this); + } +}; + +TimerRef CreateTimer() { + return std::unique_ptr(new TimerImplHtml); +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuItemImplHtml : public MenuItem { +public: + val htmlMenuItem; + + MenuItemImplHtml() : + htmlMenuItem(val::global("document").call("createElement", val("li"))) + {} + + void SetAccelerator(KeyboardEvent accel) override { + val htmlAccel = htmlMenuItem.call("querySelector", val(".accel")); + if(htmlAccel.as()) { + htmlAccel.call("remove"); + } + htmlAccel = val::global("document").call("createElement", val("span")); + htmlAccel.call("setAttribute", val("class"), val("accel")); + htmlAccel.set("innerText", AcceleratorDescription(accel)); + htmlMenuItem.call("appendChild", htmlAccel); + } + + void SetIndicator(Indicator type) override { + val htmlClasses = htmlMenuItem["classList"]; + htmlClasses.call("remove", val("check")); + htmlClasses.call("remove", val("radio")); + switch(type) { + case Indicator::NONE: + break; + + case Indicator::CHECK_MARK: + htmlClasses.call("add", val("check")); + break; + + case Indicator::RADIO_MARK: + htmlClasses.call("add", val("radio")); + break; + } + } + + void SetEnabled(bool enabled) override { + if(enabled) { + htmlMenuItem["classList"].call("remove", val("disabled")); + } else { + htmlMenuItem["classList"].call("add", val("disabled")); + } + } + + void SetActive(bool active) override { + if(active) { + htmlMenuItem["classList"].call("add", val("active")); + } else { + htmlMenuItem["classList"].call("remove", val("active")); + } + } +}; + +class MenuImplHtml; + +static std::shared_ptr popupMenuOnScreen; + +class MenuImplHtml : public Menu, + public std::enable_shared_from_this { +public: + val htmlMenu; + + std::vector> menuItems; + std::vector> subMenus; + + std::function popupDismissFunc; + + MenuImplHtml() : + htmlMenu(val::global("document").call("createElement", val("ul"))) + { + htmlMenu["classList"].call("add", val("menu")); + } + + MenuItemRef AddItem(const std::string &label, std::function onTrigger, + bool mnemonics = true) override { + std::shared_ptr menuItem = std::make_shared(); + menuItems.push_back(menuItem); + menuItem->onTrigger = onTrigger; + + if(mnemonics) { + val::global("window").call("setLabelWithMnemonic", menuItem->htmlMenuItem, + Wrap(label)); + } else { + val htmlLabel = val::global("document").call("createElement", val("span")); + htmlLabel["classList"].call("add", val("label")); + htmlLabel["innerText"] = Wrap(label); + menuItem->htmlMenuItem.call("appendChild", htmlLabel); + } + menuItem->htmlMenuItem.call("addEventListener", val("trigger"), + Wrap(&menuItem->onTrigger)); + htmlMenu.call("appendChild", menuItem->htmlMenuItem); + return menuItem; + } + + std::shared_ptr AddSubMenu(const std::string &label) override { + val htmlMenuItem = val::global("document").call("createElement", val("li")); + val::global("window").call("setLabelWithMnemonic", htmlMenuItem, Wrap(label)); + htmlMenuItem["classList"].call("add", val("has-submenu")); + htmlMenu.call("appendChild", htmlMenuItem); + + std::shared_ptr subMenu = std::make_shared(); + subMenus.push_back(subMenu); + htmlMenuItem.call("appendChild", subMenu->htmlMenu); + return subMenu; + } + + void AddSeparator() override { + val htmlSeparator = val::global("document").call("createElement", val("li")); + htmlSeparator["classList"].call("add", val("separator")); + htmlMenu.call("appendChild", htmlSeparator); + } + + void PopUp() override { + if(popupMenuOnScreen) { + popupMenuOnScreen->htmlMenu.call("remove"); + popupMenuOnScreen = NULL; + } + + EmscriptenMouseEvent emStatus = {}; + sscheck(emscripten_get_mouse_status(&emStatus)); + htmlMenu["classList"].call("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("appendChild", htmlMenu); + popupMenuOnScreen = shared_from_this(); + } + + void Clear() override { + while(htmlMenu["childElementCount"].as() > 0) { + htmlMenu["firstChild"].call("remove"); + } + } +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +class MenuBarImplHtml final : public MenuBar { +public: + val htmlMenuBar; + + std::vector> subMenus; + + MenuBarImplHtml() : + htmlMenuBar(val::global("document").call("createElement", val("ul"))) + { + htmlMenuBar["classList"].call("add", val("menu")); + htmlMenuBar["classList"].call("add", val("menubar")); + } + + std::shared_ptr AddSubMenu(const std::string &label) override { + val htmlMenuItem = val::global("document").call("createElement", val("li")); + val::global("window").call("setLabelWithMnemonic", htmlMenuItem, Wrap(label)); + htmlMenuBar.call("appendChild", htmlMenuItem); + + std::shared_ptr subMenu = std::make_shared(); + subMenus.push_back(subMenu); + htmlMenuItem.call("appendChild", subMenu->htmlMenu); + return subMenu; + } + + void Clear() override { + while(htmlMenuBar["childElementCount"].as() > 0) { + htmlMenuBar["firstChild"].call("remove"); + } + } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// 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 editingDoneFunc; + std::shared_ptr menuBar; + + WindowImplHtml(val htmlContainer, std::string emCanvasId) : + emCanvasId(emCanvasId), + htmlContainer(htmlContainer), + htmlEditor(val::global("document").call("createElement", val("input"))) + { + htmlEditor["classList"].call("add", val("editor")); + htmlEditor["style"].set("display", "none"); + editingDoneFunc = [this] { + if(onEditingDone) { + onEditingDone(Unwrap(htmlEditor["value"])); + } + }; + htmlEditor.call("addEventListener", val("trigger"), Wrap(&editingDoneFunc)); + htmlContainer.call("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("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("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("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(); + 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 menuBarImpl = + std::static_pointer_cast(menuBar); + this->menuBar = menuBarImpl; + + val htmlBody = val::global("document")["body"]; + val htmlCurrentMenuBar = htmlBody.call("querySelector", val(".menubar")); + if(htmlCurrentMenuBar.as()) { + htmlCurrentMenuBar.call("remove"); + } + htmlBody.call("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() != "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("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("getElementById", htmlContainerId); + std::string emCanvasId = std::string("canvas") + std::to_string(windowNum); + + windowNum++; + return std::make_shared(htmlContainer, emCanvasId); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +void Open3DConnexion() {} +void Close3DConnexion() {} +void Request3DConnexionEventsForWindow(WindowRef window) {} + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplHtml; + +static std::vector> dialogsOnScreen; + +class MessageDialogImplHtml final : public MessageDialog, + public std::enable_shared_from_this { +public: + val htmlModal; + val htmlDialog; + val htmlMessage; + val htmlDescription; + val htmlButtons; + + std::vector> responseFuncs; + + MessageDialogImplHtml() : + htmlModal(val::global("document").call("createElement", val("div"))), + htmlDialog(val::global("document").call("createElement", val("div"))), + htmlMessage(val::global("document").call("createElement", val("strong"))), + htmlDescription(val::global("document").call("createElement", val("p"))), + htmlButtons(val::global("document").call("createElement", val("div"))) + { + htmlModal["classList"].call("add", val("modal")); + htmlModal.call("appendChild", htmlDialog); + htmlDialog["classList"].call("add", val("dialog")); + htmlDialog.call("appendChild", htmlMessage); + htmlDialog.call("appendChild", htmlDescription); + htmlButtons["classList"].call("add", val("buttons")); + htmlDialog.call("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("createElement", val("div")); + htmlButton["classList"].call("add", val("button")); + val::global("window").call("setLabelWithMnemonic", htmlButton, Wrap(label)); + if(isDefault) { + htmlButton["classList"].call("add", val("selected")); + } + + std::function responseFunc = [this, response] { + htmlModal.call("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("addEventListener", val("trigger"), Wrap(&responseFuncs.back())); + + htmlButtons.call("appendChild", htmlButton); + } + + Response RunModal() { + ssassert(false, "RunModal not supported on Emscripten"); + } + + void ShowModal() { + dialogsOnScreen.push_back(shared_from_this()); + val::global("document")["body"].call("appendChild", htmlModal); + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplHtml : public FileDialog { +public: + // FIXME(emscripten): implement +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + // FIXME(emscripten): implement + return std::shared_ptr(); + +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + // FIXME(emscripten): implement + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + return {}; +} + +void OpenInBrowser(const std::string &url) { + val::global("window").call("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); +} + +} +} diff --git a/src/platform/html/emshell.html b/src/platform/html/emshell.html new file mode 100644 index 00000000..62b3e355 --- /dev/null +++ b/src/platform/html/emshell.html @@ -0,0 +1,82 @@ + +SolveSpace Web Edition (EXPERIMENTAL)
+
+
+
Downloading...
+ + +
+
{{{ SCRIPT }}}