From 55960fb0b4995d08dc14edde21f82c6decef292e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 4 Jun 2019 20:03:08 +0200 Subject: [PATCH 01/35] Animation: use float literals everywhere. Makes it easier to copypaste into GLSL ES. --- src/Magnum/Animation/Easing.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Magnum/Animation/Easing.h b/src/Magnum/Animation/Easing.h index a53a2aaefb..a932531f78 100644 --- a/src/Magnum/Animation/Easing.h +++ b/src/Magnum/Animation/Easing.h @@ -731,7 +731,7 @@ Combination of @ref circularIn() and @ref circularOut(). @see @ref sineInOut() */ inline Float circularInOut(Float t) { - if(t < 0.5f) return 0.5f*(1.0f - std::sqrt(1.0f - 4*t*t)); + if(t < 0.5f) return 0.5f*(1.0f - std::sqrt(1.0f - 4.0f*t*t)); return 0.5f*(1.0f + std::sqrt(-4.0f*t*t + 8.0f*t - 3.0f)); } From cf1c78f93c0e9c38094bbaac8e5191520f704c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 4 Jun 2019 20:03:40 +0200 Subject: [PATCH 02/35] GL: ffs, a time query and nowhere it mentions *what* is the time unit. --- src/Magnum/GL/TimeQuery.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Magnum/GL/TimeQuery.h b/src/Magnum/GL/TimeQuery.h index 2b63da9c7f..60f8e46ba3 100644 --- a/src/Magnum/GL/TimeQuery.h +++ b/src/Magnum/GL/TimeQuery.h @@ -47,7 +47,8 @@ usage of both methods: @snippet MagnumGL.cpp TimeQuery-usage2 -Using the latter results in fewer OpenGL calls when doing more measures. +Using the latter results in fewer OpenGL calls when doing more measures. All +times are reported in nanoseconds. @requires_gl33 Extension @gl_extension{ARB,timer_query} @requires_es_extension Extension @gl_extension{EXT,disjoint_timer_query} @@ -66,8 +67,8 @@ class TimeQuery: public AbstractQuery { */ enum class Target: GLenum { /** - * Elapsed time. Use @ref result() or @ref result() - * to retrieve the result. + * Elapsed time, in nanoseconds. Use @ref result() or + * @ref result() to retrieve the result. * @see @ref timestamp() */ #ifndef MAGNUM_TARGET_GLES @@ -77,9 +78,9 @@ class TimeQuery: public AbstractQuery { #endif /** - * Timestamp. For use with @ref timestamp() only, use - * @ref result() or @ref result() to retrieve - * the result. + * Timestamp, in nanoseconds. For use with @ref timestamp() only, + * use @ref result() or @ref result() to + * retrieve the result. */ #ifndef MAGNUM_TARGET_GLES Timestamp = GL_TIMESTAMP From b970b4d2479296b8d110357758e1ee0db6a7e7fd Mon Sep 17 00:00:00 2001 From: Squareys Date: Thu, 13 Dec 2018 13:00:58 +0100 Subject: [PATCH 03/35] Platform: Add initial EmscriptenApplication Signed-off-by: Squareys --- CMakeLists.txt | 4 + doc/platforms-html5.dox | 19 +- modules/FindMagnum.cmake | 14 +- package/ci/travis-emscripten.sh | 1 + src/Magnum/Platform/CMakeLists.txt | 46 +- src/Magnum/Platform/EmscriptenApplication.cpp | 615 ++++++++ src/Magnum/Platform/EmscriptenApplication.h | 1372 +++++++++++++++++ src/Magnum/Platform/Test/CMakeLists.txt | 33 +- .../Test/EmscriptenApplicationTest.cpp | 86 ++ .../Test/EmscriptenApplicationTest.html | 28 + 10 files changed, 2193 insertions(+), 25 deletions(-) create mode 100644 src/Magnum/Platform/EmscriptenApplication.cpp create mode 100644 src/Magnum/Platform/EmscriptenApplication.h create mode 100644 src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp create mode 100644 src/Magnum/Platform/Test/EmscriptenApplicationTest.html diff --git a/CMakeLists.txt b/CMakeLists.txt index d287276d6b..e2893a028b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,10 @@ option(WITH_EGLCONTEXT "Build EglContext library" OFF) if(CORRADE_TARGET_ANDROID) option(WITH_ANDROIDAPPLICATION "Build AndroidApplication library" OFF) +# Emscripten-specific application libraries +elseif(CORRADE_TARGET_EMSCRIPTEN) + option(WITH_EMSCRIPTENAPPLICATION "Build EmscriptenApplication library" OFF) + # iOS-specific application libraries elseif(CORRADE_TARGET_IOS) option(WITH_WINDOWLESSIOSAPPLICATION "Build WindowlessIosApplication library" OFF) diff --git a/doc/platforms-html5.dox b/doc/platforms-html5.dox index e4b77cb365..156adf547a 100644 --- a/doc/platforms-html5.dox +++ b/doc/platforms-html5.dox @@ -97,14 +97,14 @@ In case you don't have an OpenGL ES build set up yet, you need to copy Magnum source to the `modules/` dir in your project so it is able to find the WebGL libraries. -Magnum provides an Emscripten application wrapper in -@ref Platform::Sdl2Application. See its documentation for more information +Magnum provides Emscripten application wrappers in @ref Platform::Sdl2Application +and @ref Platform::EmscriptenApplication. See their documentation for more information about general usage. You can also use the Emscripten APIs directly or any other way. -@note The @ref Platform::Sdl2Application also contains a fully configured - bootstrap project that's ready to build and deploy. Check its documentation - for details. +@note @ref Platform::Sdl2Application and @ref Platform::EmscriptenApplication + also contains a fully configured bootstrap projects that are ready to build + and deploy. Check their documentation for details. To target the web browser, you need to provide a HTML markup for your application. Template one is below. The markup references two files, @@ -350,8 +350,11 @@ Module.doNotCaptureKeyboard = true; @endcode -The above is implicitly set for windowless apps, because these don't have any -event loop. +The above is implicitly set for @ref Platform::WindowlessEglApplicaiton, because +it does not have an event loop. + +@cb{.js} Module.doNotCaptureKeyboard @ce is not supported by +@ref Platform::EmscriptenApplication. Another solution is to specify the element on which it should capture keybard using @cb{.js} Module.keyboardListeningElement @ce --- it requires the actual @@ -367,7 +370,7 @@ like this: After that, the canvas can be focused with a @m_class{m-label m-default} **Tab** key. But because Emscripten eats all mouse input, the `mousedown` event won't -be propagated to focus the canvas unlesss you do that manually: +be propagated to focus the canvas unless you do that manually: @code{.js} Module.keyboardListeningElement = Module.canvas; diff --git a/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index f8da1e6076..06f08fb51e 100644 --- a/modules/FindMagnum.cmake +++ b/modules/FindMagnum.cmake @@ -67,6 +67,8 @@ # TextureTools - TextureTools library # Trade - Trade library # Vk - Vk library +# AndroidApplication - Android application +# EmscriptenApplication - Emscripten application # GlfwApplication - GLFW application # GlxApplication - GLX application # Sdl2Application - SDL2 application @@ -338,10 +340,10 @@ endif() set(_MAGNUM_LIBRARY_COMPONENT_LIST Audio DebugTools GL MeshTools Primitives SceneGraph Shaders Text TextureTools Trade Vk - AndroidApplication GlfwApplication GlxApplication Sdl2Application - XEglApplication WindowlessCglApplication WindowlessEglApplication - WindowlessGlxApplication WindowlessIosApplication WindowlessWglApplication - WindowlessWindowsEglApplication + AndroidApplication EmscriptenApplication GlfwApplication GlxApplication + Sdl2Application XEglApplication WindowlessCglApplication + WindowlessEglApplication WindowlessGlxApplication WindowlessIosApplication + WindowlessWglApplication WindowlessWindowsEglApplication CglContext EglContext GlxContext WglContext OpenGLTester) set(_MAGNUM_PLUGIN_COMPONENT_LIST @@ -410,6 +412,10 @@ endif() set(_MAGNUM_Trade_DEPENDENCIES ) set(_MAGNUM_AndroidApplication_DEPENDENCIES GL) +set(_MAGNUM_EmscriptenApplication_DEPENDENCIES) +if(MAGNUM_TARGET_GL) + list(APPEND _MAGNUM_EmscriptenApplication_DEPENDENCIES GL) +endif() set(_MAGNUM_GlfwApplication_DEPENDENCIES ) if(MAGNUM_TARGET_GL) diff --git a/package/ci/travis-emscripten.sh b/package/ci/travis-emscripten.sh index dfb6ca0bb6..8d91fd9871 100755 --- a/package/ci/travis-emscripten.sh +++ b/package/ci/travis-emscripten.sh @@ -50,6 +50,7 @@ cmake .. \ -DCMAKE_FIND_ROOT_PATH=$HOME/deps \ -DWITH_AUDIO=ON \ -DWITH_VK=OFF \ + -DWITH_EMSCRIPTENAPPLICATION=ON \ -DWITH_SDL2APPLICATION=ON \ -DWITH_WINDOWLESSEGLAPPLICATION=ON \ -DWITH_ANYAUDIOIMPORTER=ON \ diff --git a/src/Magnum/Platform/CMakeLists.txt b/src/Magnum/Platform/CMakeLists.txt index 1eab983be2..8e4ae69bc1 100644 --- a/src/Magnum/Platform/CMakeLists.txt +++ b/src/Magnum/Platform/CMakeLists.txt @@ -3,6 +3,7 @@ # # Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 # Vladimír Vondruš +# Copyright © 2018, 2019 Jonathan Hale # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -35,9 +36,9 @@ set(MagnumPlatform_HEADERS set(MagnumPlatform_PRIVATE_HEADERS ) -# DPI scaling queries only for Sdl2Application and GlfwApplication at the -# moment, build the files only then -if(WITH_GLFWAPPLICATION OR WITH_SDL2APPLICATION) +# DPI scaling queries only for EmscriptenApplication, Sdl2Application and +# GlfwApplication at the moment, build the files only then +if(WITH_EMSCRIPTENAPPLICATION OR WITH_GLFWAPPLICATION OR WITH_SDL2APPLICATION) # List of libraries to link when using the MagnumPlatformObjects target # TODO: use target_link_libraries() when we are on a CMake version that # supports it (3.12?) @@ -170,6 +171,45 @@ if(WITH_ANDROIDAPPLICATION) add_library(Magnum::AndroidApplication ALIAS MagnumAndroidApplication) endif() +# Emscripten application +if(WITH_EMSCRIPTENAPPLICATION) + if(NOT CORRADE_TARGET_EMSCRIPTEN) + message(FATAL_ERROR "EmscriptenApplication is available only when targeting Emscripten. Set WITH_EMSCRIPTENAPPLICATION to OFF to skip building it.") + endif() + + set(MagnumEmscriptenApplication_SRCS + $ + EmscriptenApplication.cpp) + set(MagnumEmscriptenApplication_HEADERS + EmscriptenApplication.h) + + add_library(MagnumEmscriptenApplication STATIC + ${MagnumEmscriptenApplication_SRCS} + ${MagnumEmscriptenApplication_HEADERS} + ${MagnumEmscriptenApplication_PRIVATE_HEADERS}) + set_target_properties(MagnumEmscriptenApplication PROPERTIES + DEBUG_POSTFIX "-d" + FOLDER "Magnum/Platform") + # TODO: use MagnumPlatformObjects instead of ${MagnumPlatform_*} when + # CMake supports it + target_link_libraries(MagnumEmscriptenApplication PUBLIC Magnum + ${MagnumPlatform_LINK_LIBRARIES}) + target_compile_definitions(MagnumEmscriptenApplication PRIVATE + ${MagnumPlatform_COMPILE_DEFINITIONS}) + if(TARGET_GL) + target_link_libraries(MagnumEmscriptenApplication PUBLIC MagnumGL) + endif() + + install(FILES ${MagnumEmscriptenApplication_HEADERS} DESTINATION ${MAGNUM_INCLUDE_INSTALL_DIR}/Platform) + install(TARGETS MagnumEmscriptenApplication + RUNTIME DESTINATION ${MAGNUM_BINARY_INSTALL_DIR} + LIBRARY DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR} + ARCHIVE DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR}) + + # Magnum EmscriptenApplication target alias for superprojects + add_library(Magnum::EmscriptenApplication ALIAS MagnumEmscriptenApplication) +endif() + # GLFW application if(WITH_GLFWAPPLICATION) find_package(GLFW) diff --git a/src/Magnum/Platform/EmscriptenApplication.cpp b/src/Magnum/Platform/EmscriptenApplication.cpp new file mode 100644 index 0000000000..91c64be559 --- /dev/null +++ b/src/Magnum/Platform/EmscriptenApplication.cpp @@ -0,0 +1,615 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + Copyright © 2018, 2019 Jonathan Hale + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "EmscriptenApplication.h" + +#include +#include + +#include +#include +#include +#include + +#include "Magnum/Math/ConfigurationValue.h" + +#ifdef MAGNUM_TARGET_GL +#include "Magnum/GL/Version.h" +#include "Magnum/Platform/GLContext.h" +#endif + +#include "Magnum/Platform/Implementation/DpiScaling.h" + +namespace Magnum { namespace Platform { + +namespace { + typedef EmscriptenApplication::KeyEvent::Key Key; + + /* Entry for key name to `Key` enum mapping */ + struct Entry { + const char* key; + Key value; + }; + + /* Key name to `Key` enum mapping. These need to be sorted for use with + std::lower_bound in KeyEvent::toKey */ + constexpr Entry KeyMapping[]{ + {"AltLeft", Key::LeftAlt}, + {"AltRight", Key::RightAlt}, + {"ArrowDown", Key::Down}, + {"ArrowLeft", Key::Left}, + {"ArrowRight", Key::Right}, + {"ArrowUp", Key::Up}, + {"Backslash", Key::Backslash}, + {"Backspace", Key::Backspace}, + {"CapsLock", Key::CapsLock}, + {"Comma", Key::Comma}, + {"ContextMenu", Key::Menu}, + {"ControlLeft", Key::LeftCtrl}, + {"ControlRight", Key::RightCtrl}, + {"Delete", Key::Delete}, + {"End", Key::End}, + {"Enter", Key::Enter}, + {"Equal", Key::Equal}, + {"Escape", Key::Esc}, + {"Home", Key::Home}, + {"Insert", Key::Insert}, + {"MetaLeft", Key::LeftSuper}, + {"MetaRight", Key::RightSuper}, + {"Minus", Key::Minus}, + {"NumLock", Key::NumLock}, + {"PageDown", Key::PageDown}, + {"PageUp", Key::PageUp}, + {"Pause", Key::Pause}, + {"Period", Key::Period}, + {"Plus", Key::Plus}, + {"PrintScreen", Key::PrintScreen}, + {"Quote", Key::Quote}, + {"ScrollLock", Key::ScrollLock}, + {"ShiftLeft", Key::LeftShift}, + {"ShiftRight", Key::RightShift}, + {"Slash", Key::Slash}, + {"Space", Key::Space}, + {"Tab", Key::Tab}, + }; + + /* Predicate for Entry "less than" to use with std::lower_bound */ + struct EntryCompare { + bool operator()(const Entry& a, const char* const& b) { + return std::strcmp(a.key, b) < 0; + } + + bool operator()(const char*& a, const Entry& b) { + return std::strcmp(a, b.key) < 0; + } + }; + + /* Translate emscripten key code (as defined by + https://www.w3.org/TR/uievents-code/#key-code-attribute-value) + to Key enum. + + @param key Keyboard layout dependent key string, e.g. 'a', or '-' + @param code Keyboard layout independent key string, e.g. 'KeyA' or 'Minus'. + Note that the y key on some layouts may result in 'KeyZ'. + */ + Key toKey(const EM_UTF8* key, const EM_UTF8* code) { + const size_t keyLength = std::strlen(key); + if(keyLength == 0) return Key::Unknown; + + /* We use key for a-z as it gives us a keyboard layout respecting + representation of the key, i.e. we get `z` for z depending on layout + where code may give us `y` independent of the layout. */ + if(keyLength == 1) { + if(key[0] >= 'a' && key[0] <= 'z') return Key(key[0]); + else if(key[0] >= 'A' && key[0] <= 'Z') return Key(key[0] - 'A' + 'a'); + } + + /* We use code for 0-9 as it allows us to differentiate towards Numpad digits. + For digits independent of numpad or not, key is e.g. '0' for Zero */ + const size_t codeLength = std::strlen(code); + if(Utility::String::viewBeginsWith({code, codeLength}, "Digit")) { + return Key(code[5]); + + /* Numpad keys */ + } else if(Utility::String::viewBeginsWith({code, codeLength}, "Numpad")) { + std::string numKey(code + 6); + if(numKey == "Add") return Key::NumAdd; + if(numKey == "Decimal") return Key::NumDecimal; + if(numKey == "Divide") return Key::NumDivide; + if(numKey == "Enter") return Key::NumEnter; + if(numKey == "Equal") return Key::NumEqual; + if(numKey == "Multiply") return Key::NumMultiply; + if(numKey == "Subtract") return Key::NumSubtract; + + /* Numpad0 - Numpad9 */ + const Int num = numKey[0] - '0'; + if(num >= 0 && num <= 9) { + return Key(num + Int(Key::NumZero)); + } + + return Key::Unknown; + } + + const auto mapping = Containers::arrayView(KeyMapping, + Containers::arraySize(KeyMapping)); + const Entry* found = + std::lower_bound(mapping.begin(), mapping.end(), code, EntryCompare{}); + if(found != mapping.end() && std::strcmp(found->key, code) == 0) { + return found->value; + } + + /* F1 - F12 */ + if(code[0] == 'F') { + /* F1-F9 */ + if(code[2] != '\0') { + const Int num = code[2] - '0'; + return Key(Int(Key::F10) + num); + } + /* F10-F12 */ + const Int num = code[1] - '1'; + return Key(Int(Key::F1) + num); + } + + return Key::Unknown; + } + +} + +#ifdef MAGNUM_TARGET_GL +EmscriptenApplication::EmscriptenApplication(const Arguments& arguments): EmscriptenApplication{arguments, Configuration{}, GLConfiguration{}} {} + +EmscriptenApplication::EmscriptenApplication(const Arguments& arguments, const Configuration& configuration): EmscriptenApplication{arguments, configuration, GLConfiguration{}} {} + +EmscriptenApplication::EmscriptenApplication(const Arguments& arguments, const Configuration& configuration, const GLConfiguration& glConfiguration): EmscriptenApplication{arguments, NoCreate} { + create(configuration, glConfiguration); +} +#endif + +EmscriptenApplication::EmscriptenApplication(const Arguments& arguments, NoCreateT): + _flags{Flag::Redraw} +{ + Utility::Arguments args{Implementation::windowScalingArguments()}; + #ifdef MAGNUM_TARGET_GL + _context.reset(new GLContext{NoCreate, args, arguments.argc, arguments.argv}); + #else + args.parse(arguments.argc, arguments.argv); + #endif + + /* Save command-line arguments */ + if(args.value("log") == "verbose") _verboseLog = true; + const std::string dpiScaling = args.value("dpi-scaling"); + + /* Use physical DPI scaling */ + if(dpiScaling == "default" || dpiScaling == "physical") { + + /* Use explicit dpi scaling vector */ + } else if(dpiScaling.find_first_of(" \t\n") != std::string::npos) + _commandLineDpiScaling = args.value("dpi-scaling"); + + /* Use explicit dpi scaling scalar */ + else + _commandLineDpiScaling = Vector2{args.value("dpi-scaling")}; +} + +EmscriptenApplication::~EmscriptenApplication() { + emscripten_webgl_make_context_current(0); +} + +void EmscriptenApplication::create() { + create(Configuration{}); +} + +void EmscriptenApplication::create(const Configuration& configuration) { + if(!tryCreate(configuration)) exit(1); +} + +#ifdef MAGNUM_TARGET_GL +void EmscriptenApplication::create(const Configuration& configuration, const GLConfiguration& glConfiguration) { + if(!tryCreate(configuration, glConfiguration)) exit(32); +} +#endif + +Vector2 EmscriptenApplication::dpiScaling(const Configuration& configuration) const { + std::ostream* verbose = _verboseLog ? Debug::output() : nullptr; + + /* Use values from the configuration only if not overriden on command line. + In any case explicit scaling has a precedence before the policy. */ + if(!_commandLineDpiScaling.isZero()) { + Debug{verbose} << "Platform::EmscriptenApplication: user-defined DPI scaling" << _commandLineDpiScaling.x(); + return _commandLineDpiScaling; + } else if(!configuration.dpiScaling().isZero()) { + Debug{verbose} << "Platform::EmscriptenApplication: app-defined DPI scaling" << _commandLineDpiScaling.x(); + return configuration.dpiScaling(); + } + + /* Take device pixel ratio on Emscripten */ + const Vector2 dpiScaling{Implementation::emscriptenDpiScaling()}; + Debug{verbose} << "Platform::EmscriptenApplication: physical DPI scaling" << dpiScaling.x(); + return dpiScaling; +} + +bool EmscriptenApplication::tryCreate(const Configuration& configuration) { + #ifdef MAGNUM_TARGET_GL + if(!(configuration.windowFlags() & Configuration::WindowFlag::Contextless)) { + return tryCreate(configuration, GLConfiguration{}); + } + #endif + if(configuration.windowFlags() & Configuration::WindowFlag::Resizable) { + _flags |= Flag::Resizable; + } + + _dpiScaling = dpiScaling(configuration); + + /* Resize window and match it to the selected format */ + const Vector2i canvasSizei{windowSize()}; + _lastKnownCanvasSize = canvasSizei; + const Vector2i size = _dpiScaling*canvasSizei; + emscripten_set_canvas_element_size("#canvas", size.x(), size.y()); + + setupCallbacks(); + + return true; +} + +#ifdef MAGNUM_TARGET_GL +bool EmscriptenApplication::tryCreate(const Configuration& configuration, const GLConfiguration& glConfiguration) { + CORRADE_ASSERT(_context->version() == GL::Version::None, "Platform::EmscriptenApplication::tryCreate(): window with OpenGL context already created", false); + if(configuration.windowFlags() & Configuration::WindowFlag::Resizable) { + _flags |= Flag::Resizable; + } + + _dpiScaling = dpiScaling(configuration); + + /* Create emscripten WebGL context */ + EmscriptenWebGLContextAttributes attrs; + emscripten_webgl_init_context_attributes(&attrs); + attrs.alpha = glConfiguration.colorBufferSize().a() > 0; + attrs.depth = glConfiguration.depthBufferSize() > 0; + attrs.stencil = glConfiguration.stencilBufferSize() > 0; + attrs.antialias = glConfiguration.sampleCount() > 0; + + attrs.premultipliedAlpha = + !!(glConfiguration.flags() & GLConfiguration::Flag::PremultipliedAlpha); + attrs.preserveDrawingBuffer = + !!(glConfiguration.flags() & GLConfiguration::Flag::PreserveDrawingBuffer); + /* powerPreference replaced preferLowPowerToHighPerformance in emscripten + version 1.38.26 */ + #ifdef EM_WEBGL_POWERPREFERENCE_LOW_POWER + attrs.powerPreference = + !!(glConfiguration.flags() & GLConfiguration::Flag::PreferLowPowerToHighPerformance) + ? EM_WEBGL_POWERPREFERENCE_LOW_POWER : EM_WEBGL_POWERPREFERENCE_HIGH_PERFORMANCE; + #else + attrs.preferLowPowerToHighPerformance = + !!(glConfiguration.flags() & GLConfiguration::Flag::PreferLowPowerToHighPerformance); + #endif + attrs.explicitSwapControl = + !!(glConfiguration.flags() & GLConfiguration::Flag::ExplicitSwapControl); + attrs.failIfMajorPerformanceCaveat = + !!(glConfiguration.flags() & GLConfiguration::Flag::FailIfMajorPerformanceCaveat); + attrs.enableExtensionsByDefault = + !!(glConfiguration.flags() & GLConfiguration::Flag::EnableExtensionsByDefault); + + #ifdef MAGNUM_TARGET_GLES3 + /* WebGL 2 */ + attrs.majorVersion = 2; + #elif defined(MAGNUM_TARGET_GLES2) + /* WebGL 1 */ + attrs.minorVersion = 1; + #else + #error unsupported OpenGL ES version + #endif + + /* Resize window and match it to the selected format */ + const Vector2i canvasSizei{windowSize()}; + _lastKnownCanvasSize = canvasSizei; + const Vector2i size = _dpiScaling*canvasSizei; + emscripten_set_canvas_element_size("#canvas", size.x(), size.y()); + + /* Create surface and context */ + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context = + emscripten_webgl_create_context("#canvas", &attrs); + if(!context) { + /* When context creation fails, `context` is a negative integer matching + EMSCRIPTEN_RESULT_* defines */ + Error{} << "Platform::EmscriptenApplication::tryCreate(): cannot create WebGL context (EMSCRIPTEN_RESULT" + << context << Debug::nospace << ")"; + return false; + } + + /* Make the context current */ + CORRADE_INTERNAL_ASSERT_OUTPUT( + emscripten_webgl_make_context_current(context) == EMSCRIPTEN_RESULT_SUCCESS); + + setupCallbacks(); + + /* Return true if the initialization succeeds */ + return _context->tryCreate(); +} +#endif + +Vector2i EmscriptenApplication::windowSize() const { + double w, h; + emscripten_get_element_css_size("#canvas", &w, &h); + return {Int(w), Int(h)}; +} + +void EmscriptenApplication::swapBuffers() { + emscripten_webgl_commit_frame(); +} + +void EmscriptenApplication::setupCallbacks() { + emscripten_set_mousedown_callback("#canvas", this, false, + [](int, const EmscriptenMouseEvent* event, void* userData) -> Int { + MouseEvent e{event}; + reinterpret_cast(userData)->mousePressEvent(e); + return e.isAccepted(); + }); + + emscripten_set_mouseup_callback("#canvas", this, false, + [](int, const EmscriptenMouseEvent* event, void* userData) -> Int { + MouseEvent e{event}; + reinterpret_cast(userData)->mouseReleaseEvent(e); + return e.isAccepted(); + }); + + emscripten_set_mousemove_callback("#canvas", this, false, + [](int, const EmscriptenMouseEvent* event, void* userData) -> Int { + MouseMoveEvent e{event}; + reinterpret_cast(userData)->mouseMoveEvent(e); + return e.isAccepted(); + }); + + emscripten_set_wheel_callback("#canvas", this, false, + [](int, const EmscriptenWheelEvent* event, void* userData) -> Int { + MouseScrollEvent e{event}; + reinterpret_cast(userData)->mouseScrollEvent(e); + return e.isAccepted(); + }); + + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + /* document and window are 'specialEventTargets' in emscripten, matching + EMSCRIPTEN_EVENT_TARGET_DOCUMENT and EMSCRIPTEN_EVENT_TARGET_WINDOW. + As the lookup happens with the passed parameter and arrays support + element lookup via strings, we can unify the code by returning string of + 1 or 2 if the target is document or window. This changed in Emscripten + 1.38.27 depending on -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 + but we don't want to force this flag on the users so the behavior + handles both. */ + #ifdef EMSCRIPTEN_EVENT_TARGET_DOCUMENT + char* const keyboardListeningElement = reinterpret_cast(EM_ASM_INT({ + var element = Module['keyboardListeningElement'] || document; + + if(element === document) return 1; + if(element === window) return 2; + if('id' in element) + return allocate(intArrayFromString(element.id), 'i8', ALLOC_NORMAL); + + return 0; + })); + #else + char* const keyboardListeningElement = reinterpret_cast(EM_ASM_INT({ + var element = Module['keyboardListeningElement'] || document; + + if(element === document) element = {id: '#document'}; + if(element === window) element = {id: '#window'}; + if('id' in element) + return allocate(intArrayFromString(element.id), 'i8', ALLOC_NORMAL); + + return 0; + })); + #endif + #pragma GCC diagnostic pop + + /* Happens only if keyboardListeningElement was set, but did not have an + `id` attribute. Instead it should be either null or undefined, a DOM + element, `window` or `document`. */ + CORRADE_ASSERT(keyboardListeningElement, + "EmscriptenApplication::setupCallbacks(): invalid value for Module['keyboardListeningElement']", ); + + /* keypress_callback does not fire for most of the keys and the modifiers + don't seem to work, keydown on the other hand works fine for all */ + emscripten_set_keydown_callback(keyboardListeningElement, this, false, + [](int, const EmscriptenKeyboardEvent* event, void* userData) -> Int { + EmscriptenApplication* app = reinterpret_cast(userData); + if(app->isTextInputActive() && std::strlen(event->key) == 1) { + TextInputEvent e{{event->key, 1}}; + app->textInputEvent(e); + return e.isAccepted(); + } + KeyEvent e{event}; + app->keyPressEvent(e); + return e.isAccepted(); + }); + + emscripten_set_keyup_callback(keyboardListeningElement, this, false, + [](int, const EmscriptenKeyboardEvent* event, void* userData) -> Int { + KeyEvent e{event}; + reinterpret_cast(userData)->keyReleaseEvent(e); + return e.isAccepted(); + }); + + #ifdef EMSCRIPTEN_EVENT_TARGET_DOCUMENT + if(keyboardListeningElement != EMSCRIPTEN_EVENT_TARGET_DOCUMENT && + keyboardListeningElement != EMSCRIPTEN_EVENT_TARGET_WINDOW) + #endif + { + std::free(keyboardListeningElement); + } +} + +void EmscriptenApplication::startTextInput() { + _flags |= Flag::TextInputActive; +} + +void EmscriptenApplication::stopTextInput() { + _flags &= ~Flag::TextInputActive; +} + +void EmscriptenApplication::setTextInputRect(const Range2Di&) { + // TODO: Place a hidden input field at given rect +} + +void EmscriptenApplication::viewportEvent(ViewportEvent&) {} +void EmscriptenApplication::keyPressEvent(KeyEvent&) {} +void EmscriptenApplication::keyReleaseEvent(KeyEvent&) {} +void EmscriptenApplication::mousePressEvent(MouseEvent&) {} +void EmscriptenApplication::mouseReleaseEvent(MouseEvent&) {} +void EmscriptenApplication::mouseMoveEvent(MouseMoveEvent&) {} +void EmscriptenApplication::mouseScrollEvent(MouseScrollEvent&) {} +void EmscriptenApplication::textInputEvent(TextInputEvent&) {} + +EmscriptenApplication::GLConfiguration::GLConfiguration(): + _colorBufferSize{8, 8, 8, 0}, _depthBufferSize{24}, _stencilBufferSize{0} {} + +void EmscriptenApplication::mainLoopIteration() { + /* The resize event is not fired on window resize, so poll for the canvas + size here. But only if the window was requested to be resizable, to + avoid resizing the canvas when the user doesn't want that. Related + issue: https://github.com/kripken/emscripten/issues/1731 + + As this is caused by the DOM3 events spec only requiring browsers to + fire the resize event for `window` not generally for all DOM elemenets, + it also applies to `emscripten_set_resize_callback`. */ + if(_flags & Flag::Resizable) { + /* Emscripten 1.38.27 changed to generic CSS selectors from element + IDs depending on -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 + being set (which we can't detect at compile time). See above for the + reason why we hardcode #canvas here. */ + const Vector2i canvasSizei{windowSize()}; + if(canvasSizei != _lastKnownCanvasSize) { + _lastKnownCanvasSize = canvasSizei; + const Vector2i size = _dpiScaling*canvasSizei; + emscripten_set_canvas_element_size("#canvas", size.x(), size.y()); + #ifdef MAGNUM_TARGET_GL + ViewportEvent e{size, size, _dpiScaling}; + #else + ViewportEvent e{size, _dpiScaling}; + #endif + viewportEvent(e); + _flags |= Flag::Redraw; + } + } + + if(_flags & Flag::Redraw) { + _flags &= ~Flag::Redraw; + drawEvent(); + } +} + +void EmscriptenApplication::exec() { + emscripten_set_main_loop_arg([](void* arg) { + static_cast(arg)->mainLoopIteration(); + }, this, 0, true); +} + +void EmscriptenApplication::exit(int) { + emscripten_cancel_main_loop(); +} + +EmscriptenApplication::MouseEvent::Button EmscriptenApplication::MouseEvent::button() const { + return Button(1 << _event->button); +} + +Vector2i EmscriptenApplication::MouseEvent::position() const { + return {Int(_event->canvasX), Int(_event->canvasY)}; +} + +EmscriptenApplication::MouseEvent::Modifiers EmscriptenApplication::MouseEvent::modifiers() const { + Modifiers m; + if(_event->ctrlKey) m |= Modifier::Ctrl; + if(_event->shiftKey) m |= Modifier::Shift; + if(_event->altKey) m |= Modifier::Alt; + if(_event->metaKey) m |= Modifier::Super; + return m; +} + +EmscriptenApplication::MouseMoveEvent::Buttons EmscriptenApplication::MouseMoveEvent::buttons() const { + return EmscriptenApplication::MouseMoveEvent::Button(_event->buttons); +} + +Vector2i EmscriptenApplication::MouseMoveEvent::position() const { + return {Int(_event->canvasX), Int(_event->canvasY)}; +} + +EmscriptenApplication::MouseMoveEvent::Modifiers EmscriptenApplication::MouseMoveEvent::modifiers() const { + Modifiers m; + if(_event->ctrlKey) m |= Modifier::Ctrl; + if(_event->shiftKey) m |= Modifier::Shift; + if(_event->altKey) m |= Modifier::Alt; + if(_event->metaKey) m |= Modifier::Super; + return m; +} + +Vector2 EmscriptenApplication::MouseScrollEvent::offset() const { + /* From emscripten's Browser.getMouseWheelDelta() function in + library_browser.js: + + DOM_DELTA_PIXEL => 100 pixels = 1 step + DOM_DELTA_LINE => 3 lines = 1 step + DOM_DELTA_PAGE => 1 page = 80 steps + */ + const Float f = (_event->deltaMode == DOM_DELTA_PIXEL) ? -0.01f : + ((_event->deltaMode == DOM_DELTA_LINE) ? -1.0f/3.0f : -80.0f); + + return {f*Float(_event->deltaX), f*Float(_event->deltaY)}; +} + +Vector2i EmscriptenApplication::MouseScrollEvent::position() const { + return {Int(_event->mouse.canvasX), Int(_event->mouse.canvasY)}; +} + +EmscriptenApplication::InputEvent::Modifiers EmscriptenApplication::MouseScrollEvent::modifiers() const { + Modifiers m; + if(_event->mouse.ctrlKey) m |= Modifier::Ctrl; + if(_event->mouse.shiftKey) m |= Modifier::Shift; + if(_event->mouse.altKey) m |= Modifier::Alt; + if(_event->mouse.metaKey) m |= Modifier::Super; + return m; +} + +Key EmscriptenApplication::KeyEvent::key() const { + return toKey(_event->key, _event->code); +} + +std::string EmscriptenApplication::KeyEvent::keyName() const { + if((_event->key[0] >= 'a' && _event->key[0] <= 'z') || + (_event->key[0] >= 'A' && _event->key[0] <= 'Z')) { + return _event->key; + } + return _event->code; +} + +EmscriptenApplication::InputEvent::Modifiers EmscriptenApplication::KeyEvent::modifiers() const { + Modifiers m; + if(_event->ctrlKey) m |= Modifier::Ctrl; + if(_event->shiftKey) m |= Modifier::Shift; + if(_event->altKey) m |= Modifier::Alt; + if(_event->metaKey) m |= Modifier::Super; + return m; +} + +}} diff --git a/src/Magnum/Platform/EmscriptenApplication.h b/src/Magnum/Platform/EmscriptenApplication.h new file mode 100644 index 0000000000..7fe6dbf03f --- /dev/null +++ b/src/Magnum/Platform/EmscriptenApplication.h @@ -0,0 +1,1372 @@ +#ifndef Magnum_Platform_EmscriptenApplication_h +#define Magnum_Platform_EmscriptenApplication_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + Copyright © 2018, 2019 Jonathan Hale + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#if defined(CORRADE_TARGET_EMSCRIPTEN) || defined(DOXYGEN_GENERATING_OUTPUT) +/** @file + * @brief Class @ref Magnum::Platform::EmscriptenApplication, macro @ref MAGNUM_EMSCRIPTENAPPLICATION_MAIN() + */ +#endif + +#include + +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Tags.h" +#include "Magnum/GL/GL.h" +#include "Magnum/Math/Vector4.h" +#include "Magnum/Platform/Platform.h" + +#if defined(CORRADE_TARGET_EMSCRIPTEN) || defined(DOXYGEN_GENERATING_OUTPUT) +struct EmscriptenKeyboardEvent; +struct EmscriptenMouseEvent; +struct EmscriptenWheelEvent; + +namespace Magnum { namespace Platform { + +/** @nosubgrouping +@brief Emscripten application + +Application running on Emscripten. + +This application library is available only on +@ref CORRADE_TARGET_EMSCRIPTEN "Emscripten", see respective sections +in the @ref building-corrade-cross-emscripten "Corrade" and +@ref building-cross-emscripten "Magnum" building documentation. It is built if +`WITH_EMSCRIPTENAPPLICATION` is enabled when building Magnum. + +@section Platform-EmscriptenApplication-bootstrap Bootstrap application + +Fully contained base application using @ref EmscriptenApplication for Emscripten +build and CMake setup is available in `base-emscripten` branch of +[Magnum Bootstrap](https://github.com/mosra/magnum-bootstrap) repository, +download it as [tar.gz](https://github.com/mosra/magnum-bootstrap/archive/base-emscripten.tar.gz) +or [zip](https://github.com/mosra/magnum-bootstrap/archive/base-emscripten.zip) +file. After extracting the downloaded archive, you can build in +the same way as with @ref Sdl2Application. + +Detailed information about deployment for Emscripten and all needed boilerplate +together with a troubleshooting guide is available in @ref platforms-emscripten. + +@section Platform-EmscriptenApplication-usage General usage + +Request the `EmscriptenApplication` component of the `Magnum` package and link +to the `Magnum::EmscriptenApplication` target: + +@code{.cmake} +find_package(Magnum REQUIRED) +if(CORRADE_TARGET_EMSCRIPTEN) + find_package(Magnum REQUIRED EmscriptenApplication) +endif() + +# ... +if(CORRADE_TARGET_EMSCRIPTEN) + target_link_libraries(your-app Magnum::EmscriptenApplication) +endif() +@endcode + +If no other application is requested, you can also use the generic +`Magnum::Application` alias to simplify porting. Again, see @ref building and +@ref cmake for more information. + +In C++ code you need to implement at least @ref drawEvent() to be able to draw +on the screen. + +@code{.cpp} +class MyApplication: public Platform::EmscriptenApplication { + // implement required methods... +}; +MAGNUM_EMSCRIPTENAPPLICATION_MAIN(MyApplication) +@endcode + +If no other application header is included, this class is also aliased to +@cpp Platform::Application @ce and the macro is aliased to @cpp MAGNUM_APPLICATION_MAIN() @ce +to simplify porting. +*/ +class EmscriptenApplication { + public: + /** @brief Application arguments */ + struct Arguments { + /** @brief Constructor */ + /*implicit*/ constexpr Arguments(int& argc, char** argv) noexcept: argc{argc}, argv{argv} {} + + int& argc; /**< @brief Argument count */ + char** argv; /**< @brief Argument values */ + }; + + class Configuration; + class GLConfiguration; + class ViewportEvent; + class InputEvent; + class MouseEvent; + class MouseMoveEvent; + class MouseScrollEvent; + class KeyEvent; + class TextInputEvent; + class TextEditingEvent; + + /** + * @brief Execute main loop + * + * Calls @ref mainLoopIteration() in a loop until @ref exit() is + * called. See @ref MAGNUM_EMSCRIPTENAPPLICATION_MAIN() for usage + * information. + */ + void exec(); + + /** + * @brief Exit application main loop + * @param exitCode Exit code for compatibility with other application + * implementations + * + * Stops main loop started by @ref exec(). + */ + void exit(int exitCode = 0); + + /** + * @brief Run one iteration of application main loop + * + * Called internally from @ref exec(). If you want to have better + * control over how the main loop behaves, you can call this function + * yourself from your own `main()` function instead of it being called + * automatically from @ref exec() / @ref MAGNUM_EMSCRIPTENAPPLICATION_MAIN(). + */ + void mainLoopIteration(); + + #ifdef MAGNUM_TARGET_GL + /** + * @brief Construct with given configuration for WebGL context + * @param arguments Application arguments + * @param configuration Application configuration + * @param glConfiguration WebGL context configuration + * + * Creates application with default or user-specified configuration. + * See @ref Configuration for more information. The program exits if + * the context cannot be created, see @ref tryCreate() for an + * alternative. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + */ + explicit EmscriptenApplication(const Arguments& arguments, const Configuration& configuration, const GLConfiguration& glConfiguration); + #endif + + /** + * @brief Construct with given configuration + * + * Equivalent to calling @ref EmscriptenApplication(const Arguments&, const Configuration&, const GLConfiguration&) + * with default-constructed @ref GLConfiguration. + */ + explicit EmscriptenApplication(const Arguments& arguments, const Configuration& configuration); + + /** + * @brief Construct with default configuration + * + * Equivalent to calling @ref EmscriptenApplication(const Arguments&, const Configuration&) + * with default-constructed @ref Configuration. + */ + explicit EmscriptenApplication(const Arguments& arguments); + + /** + * @brief Construct without setting up a canvas + * @param arguments Application arguments + * + * Unlike above, the canvas is not set up and must be created later + * with @ref create() or @ref tryCreate(). + */ + explicit EmscriptenApplication(const Arguments& arguments, NoCreateT); + + /** @brief Copying is not allowed */ + EmscriptenApplication(const EmscriptenApplication&) = delete; + + /** @brief Moving is not allowed */ + EmscriptenApplication(EmscriptenApplication&&) = delete; + + virtual ~EmscriptenApplication(); + + /** @brief Copying is not allowed */ + EmscriptenApplication& operator=(const EmscriptenApplication&) = delete; + + /** @brief Moving is not allowed */ + EmscriptenApplication& operator=(EmscriptenApplication&&) = delete; + + protected: + #ifdef MAGNUM_TARGET_GL + /** + * @brief Set up a canvas with given configuration for WebGL context + * @param configuration Application configuration + * @param glConfiguration WebGL context configuration + * + * Must be called only if the context wasn't created by the constructor + * itself, i.e. when passing @ref NoCreate to it. Error message is + * printed and the program exits if the context cannot be created, see + * @ref tryCreate() for an alternative. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + */ + void create(const Configuration& configuration, const GLConfiguration& glConfiguration); + #endif + + /** + * @brief Set up a canvas with given configuration and WebGL context + * + * Equivalent to calling @ref create(const Configuration&, const GLConfiguration&) + * with default-constructed @ref GLConfiguration. + */ + void create(const Configuration& configuration); + + /** + * @brief Set up a canvas with default configuration and WebGL context + * + * Equivalent to calling @ref create(const Configuration&) with + * default-constructed @ref Configuration. + */ + void create(); + + #ifdef MAGNUM_TARGET_GL + /** + * @brief Try to create context with given configuration for WebGL context + * + * Unlike @ref create(const Configuration&, const GLConfiguration&) + * returns @cpp false @ce if the context cannot be created, + * @cpp true @ce otherwise. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + */ + bool tryCreate(const Configuration& configuration, const GLConfiguration& glConfiguration); + #endif + + /** + * @brief Try to create context with given configuration + * + * Unlike @ref create(const Configuration&) returns @cpp false @ce if + * the context cannot be created, @cpp true @ce otherwise. + */ + bool tryCreate(const Configuration& configuration); + + /** @{ @name Screen handling */ + + public: + /** + * @brief Canvas size + * + * Note that this method is named "windowSize" to be API compatible with + * Application implementations on other platforms. + * + * Window size to which all input event coordinates can be related. + */ + Vector2i windowSize() const; + + /** + * @brief DPI scaling + * + * How the content should be scaled relative to system defaults for + * given @ref windowSize(). If a window is not created yet, returns + * zero vector, use @ref dpiScaling(const Configuration&) const for + * calculating a value independently. See @ref Platform-Sdl2Application-dpi + * for more information. + * @see @ref Sdl2Application::dpiScaling(), @ref framebufferSize() + */ + Vector2 dpiScaling() const { return _dpiScaling; } + + /** + * @brief DPI scaling for given configuration + * + * Calculates DPI scaling that would be used when creating a window + * with given @p configuration. Takes into account DPI scaling policy + * and custom scaling specified on the command-line. See + * @ref Platform-Sdl2Application-dpi for more information. + */ + Vector2 dpiScaling(const Configuration& configuration) const; + + #if defined(MAGNUM_TARGET_GL) || defined(DOXYGEN_GENERATING_OUTPUT) + /** + * @brief Framebuffer size + * + * Always the same as @ref windowSize() on + * @ref CORRADE_TARGET_EMSCRIPTEN "Emscripten". See + * @ref Platform-Sdl2Application-dpi for more information. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + * + * @see @ref Sdl2Application::framebufferSize() + */ + Vector2i framebufferSize() const { return windowSize(); } + #endif + + protected: + /** + * @brief Swap buffers + * + * Paints currently rendered framebuffer on screen. + */ + void swapBuffers(); + + /** @copydoc Sdl2Application::redraw() */ + void redraw() { _flags |= Flag::Redraw; } + + private: + /** @copydoc GlfwApplication::viewportEvent(ViewportEvent&) */ + virtual void viewportEvent(ViewportEvent& event); + + /** @copydoc Sdl2Application::drawEvent() */ + virtual void drawEvent() = 0; + + /*@}*/ + + /** @{ @name Keyboard handling */ + + /** @copydoc Sdl2Application::keyPressEvent() */ + virtual void keyPressEvent(KeyEvent& event); + + /** @copydoc Sdl2Application::keyReleaseEvent() */ + virtual void keyReleaseEvent(KeyEvent& event); + + /*@}*/ + + /** @{ @name Mouse handling */ + + private: + /** + * @brief Mouse press event + * + * Called when mouse button is pressed. Default implementation does + * nothing. + */ + virtual void mousePressEvent(MouseEvent& event); + + /** + * @brief Mouse release event + * + * Called when mouse button is released. Default implementation does + * nothing. + */ + virtual void mouseReleaseEvent(MouseEvent& event); + + /** + * @brief Mouse move event + * + * Called when mouse is moved. Default implementation does nothing. + */ + virtual void mouseMoveEvent(MouseMoveEvent& event); + + /** + * @brief Mouse scroll event + * + * Called when a scrolling device is used (mouse wheel or scrolling + * area on a touchpad). Default implementation does nothing. + */ + virtual void mouseScrollEvent(MouseScrollEvent& event); + + /*@}*/ + + /** @{ @name Text input handling */ + public: + /** + * @brief Whether text input is active + * + * If text input is active, text input events go to @ref textInputEvent() + * and @ref textEditingEvent(). + * @note Note that in @ref CORRADE_TARGET_EMSCRIPTEN "Emscripten" the + * value is emulated and might not reflect external events like + * closing on-screen keyboard. + * @see @ref startTextInput(), @ref stopTextInput() + */ + bool isTextInputActive() const { + return !!(_flags & Flag::TextInputActive); + } + + /** + * @brief Start text input + * + * Starts text input that will go to @ref textInputEvent() and + * @ref textEditingEvent(). + * @see @ref stopTextInput(), @ref isTextInputActive(), + * @ref setTextInputRect() + */ + void startTextInput(); + + /** + * @brief Stop text input + * + * Stops text input that went to @ref textInputEvent() and + * @ref textEditingEvent(). + * @see @ref startTextInput(), @ref isTextInputActive(), @ref textInputEvent() + * @ref textEditingEvent() + */ + void stopTextInput(); + + /** + * @brief Set text input rectangle + * + * The @p rect defines an area where the text is being displayed, for + * example to hint the system where to place on-screen keyboard. + * @note Currently not implemented, included only for compatibility with + * other Application implementations. + */ + void setTextInputRect(const Range2Di& rect); + + private: + /** + * @brief Text input event + * + * Called when text input is active and the text is being input. + * @see @ref isTextInputActive() + */ + virtual void textInputEvent(TextInputEvent& event); + + /*@}*/ + + private: + enum class Flag: UnsignedByte { + Redraw = 1 << 0, + Resizable = 1 << 1, + TextInputActive = 1 << 2, + }; + typedef Containers::EnumSet Flags; + + CORRADE_ENUMSET_FRIEND_OPERATORS(Flags) + + void setupCallbacks(); + + Vector2 _dpiScaling; + Vector2i _lastKnownCanvasSize; + + Flags _flags; + + #ifdef MAGNUM_TARGET_GL + Containers::Pointer _context; + #endif + + /* These are saved from command-line arguments */ + bool _verboseLog{}; + Vector2 _commandLineDpiScaling; + +}; + +CORRADE_ENUMSET_OPERATORS(EmscriptenApplication::Flags) + +/** +@brief WebGL context configuration + +Double-buffered RGBA canvas with depth and stencil buffers. +@see @ref EmscriptenApplication(), @ref Configuration, @ref create(), + @ref tryCreate() +*/ +class EmscriptenApplication::GLConfiguration { + public: + /** + * @brief Context flag + * + * @see @ref Flags, @ref setFlags(), @ref Context::Flag + * @requires_gles Context flags are not available in WebGL. + */ + enum class Flag: int { + /** + * Premultiplied alpha + * + * If set, the alpha channel of the rendering context will be + * treated as representing premultiplied alpha values. If not set, the + * alpha channel represents non-premultiplied alpha. + */ + PremultipliedAlpha, + + /** + * Preserve drawing buffer + * + * If set, the contents of the drawing buffer are preserved between + * consecutive @ref EmscriptenApplication::drawEvent() calls. If not, + * color, depth and stencil are cleared at before + * @ref EmscriptenApplication::drawEvent(). + * Not setting this gives better performance. + */ + PreserveDrawingBuffer, + + /** + * Prefer low power to high performance + * + * If set, the WebGL power preference will be set to reduce power + * consumption. + */ + PreferLowPowerToHighPerformance, + + /** + * Fail if major performance caveat + * + * If set, requests context creation to abort if the browser is + * only able to create a context that does not give good hardware- + * accelerated performance. + */ + FailIfMajorPerformanceCaveat, + + /** + * Explicit swap control + * + * For more details, see the + * [Emscripten API reference](https://emscripten.org/docs/api_reference/html5.h.html#c.EmscriptenWebGLContextAttributes.explicitSwapControl) + * for more details. + */ + ExplicitSwapControl, + + /** + * Enable WebGL extensions by default + * + * For more details, see the + * [Emscripten API reference](https://emscripten.org/docs/api_reference/html5.h.html#c.EmscriptenWebGLContextAttributes.enableExtensionsByDefault) + * for more details. + */ + EnableExtensionsByDefault, + + /** + * Render via offscreen back buffer + * + * For more details, see the + * [Emscripten API reference](https://emscripten.org/docs/api_reference/html5.h.html#c.EmscriptenWebGLContextAttributes.renderViaOffscreenBackBuffer) + * for more details. + */ + RenderViaOffscreenBackBuffer, + + /** + * Proxy content to main thread + * + * For more details, see the + * [Emscripten API reference](https://emscripten.org/docs/api_reference/html5.h.html#c.EmscriptenWebGLContextAttributes.proxyContextToMainThread) + * for more details. + */ + ProxyContextToMainThread, + }; + + /** + * @brief Context flags + * + * @see @ref setFlags(), @ref Context::Flags + */ + typedef Containers::EnumSet Flags; + + /*implicit*/ GLConfiguration(); + + /** + * @brief Context flags + */ + Flags flags() const { return _flags; } + + /** + * @brief Set context flags + * @return Reference to self (for method chaining) + * + * Default is no flags. + * @see @ref addFlags(), @ref clearFlags(), @ref GL::Context::flags() + */ + GLConfiguration& setFlags(Flags flags) { + _flags = flags; + return *this; + } + + /** + * @brief Add context flags + * @return Reference to self (for method chaining) + * + * Unlike @ref setFlags(), ORs the flags with existing instead of + * replacing them. Useful for preserving the defaults. + * @see @ref clearFlags() + */ + GLConfiguration& addFlags(Flags flags) { + _flags |= flags; + return *this; + } + + /** + * @brief Clear context flags + * @return Reference to self (for method chaining) + * + * Unlike @ref setFlags(), ANDs the inverse of @p flags with existing + * instead of replacing them. Useful for removing default flags. + * @see @ref addFlags() + */ + GLConfiguration& clearFlags(Flags flags) { + _flags &= ~flags; + return *this; + } + + /** + * @brief Set context version + * + * @note This function does nothing and is included only for + * compatibility with other toolkits. @ref GL::Version::GLES200 or + * @ref GL::Version::GLES300 is used based on engine compile-time + * settings. + */ + GLConfiguration& setVersion(GL::Version) { return *this; } + + /** @brief Color buffer size */ + Vector4i colorBufferSize() const { return _colorBufferSize; } + + /** + * @brief Set color buffer size + * + * Default is @cpp {8, 8, 8, 0} @ce (8-bit-per-channel RGB, no alpha). + * @see @ref setDepthBufferSize(), @ref setStencilBufferSize() + */ + GLConfiguration& setColorBufferSize(const Vector4i& size) { + _colorBufferSize = size; + return *this; + } + + /** @brief Depth buffer size */ + Int depthBufferSize() const { return _depthBufferSize; } + + /** + * @brief Set depth buffer size + * + * Default is @cpp 24 @ce bits. + * @see @ref setColorBufferSize(), @ref setStencilBufferSize() + */ + GLConfiguration& setDepthBufferSize(Int size) { + _depthBufferSize = size; + return *this; + } + + /** @brief Stencil buffer size */ + Int stencilBufferSize() const { return _stencilBufferSize; } + + /** + * @brief Set stencil buffer size + * + * Default is @cpp 0 @ce bits (i.e., no stencil buffer). + * @see @ref setColorBufferSize(), @ref setDepthBufferSize() + */ + GLConfiguration& setStencilBufferSize(Int size) { + _stencilBufferSize = size; + return *this; + } + + /** @brief Sample count */ + Int sampleCount() const { return _sampleCount; } + + /** + * @brief Set sample count + * @return Reference to self (for method chaining) + * + * Default is @cpp 0 @ce, thus no multisampling. See also + * @ref GL::Renderer::Feature::Multisampling. + * Note that WebGL does not allow setting the sample count, but merely + * enabling or disabling multisampling. Multisampling will be enabled + * if sample count is greater than @cpp 0 @ce. + */ + GLConfiguration& setSampleCount(Int count) { + _sampleCount = count; + return *this; + } + + private: + Vector4i _colorBufferSize; + Int _depthBufferSize, _stencilBufferSize; + Int _sampleCount; + + Flags _flags; +}; + +/** +@brief Configuration + +Double-buffered RGBA canvas with depth and stencil buffers. +@see @ref EmscriptenApplication(), @ref GLConfiguration, @ref create(), + @ref tryCreate() +*/ +class EmscriptenApplication::Configuration { + public: + + /** + * @brief Window flag + * + * @see @ref WindowFlags, @ref setWindowFlags() + */ + enum class WindowFlag: UnsignedShort { + /** + * Do not create any GPU context. Use together with + * @ref EmscriptenApplication(const Arguments&), + * @ref EmscriptenApplication(const Arguments&, const Configuration&), + * @ref create(const Configuration&) or + * @ref tryCreate(const Configuration&) to prevent implicit + * creation of an WebGL context. + */ + Contextless = 1 << 0, + + Resizable = 1 << 1 + }; + + /** + * @brief Window flags + * + * @see @ref setWindowFlags() + */ + typedef Containers::EnumSet WindowFlags; + + constexpr /*implicit*/ Configuration() {} + + /** + * @brief Set window title + * @return Reference to self (for method chaining) + * + * @note This function does nothing and is included only for + * compatibility with other toolkits. You need to set the title + * separately in the `EmscriptenManifest.xml` file. + */ + template Configuration& setTitle(const T&) { return *this; } + + /** @brief Window size */ + Vector2i size() const { return _size; } + + /** + * @brief Set canvas size + * @param size Desired canvas size + * @param dpiScaling Custom DPI scaling value + * + * Default is a zero vector, meaning a value that matches the display + * or canvas size is autodetected. See @ref Platform-Sdl2Application-dpi + * for more information. + * When @p dpiScaling is not a zero vector, this function sets the DPI + * scaling directly. The resulting @ref EmscriptenApplication::windowSize() + * is @cpp size*dpiScaling @ce and @ref EmscriptenApplication::dpiScaling() + * is @p dpiScaling. + */ + Configuration& setSize(const Vector2i& size, const Vector2& dpiScaling={}) { + _size = size; + _dpiScaling = dpiScaling; + return *this; + } + + /** + * @brief Custom DPI scaling + * + * If zero, the devices pixel ratio has a priority over this value. + * The `--magnum-dpi-scaling` command-line option has a priority + * over any application-set value. + * @see @ref setSize(const Vector2i&, const Vector2&) + */ + Vector2 dpiScaling() const { return _dpiScaling; } + + /** @brief Window flags */ + WindowFlags windowFlags() const { + return _windowFlags; + } + + /** + * @brief Set window flags + * @return Reference to self (for method chaining) + * + * Default is @ref WindowFlag::Focused. + */ + Configuration& setWindowFlags(WindowFlags windowFlags) { + _windowFlags = windowFlags; + return *this; + } + + private: + Vector2i _size; + Vector2 _dpiScaling; + WindowFlags _windowFlags; +}; + +/** +@brief Viewport event + +@see @ref viewportEvent() +*/ +class EmscriptenApplication::ViewportEvent { + public: + /** @brief Copying is not allowed */ + ViewportEvent(const ViewportEvent&) = delete; + + /** @brief Moving is not allowed */ + ViewportEvent(ViewportEvent&&) = delete; + + /** @brief Copying is not allowed */ + ViewportEvent& operator=(const ViewportEvent&) = delete; + + /** @brief Moving is not allowed */ + ViewportEvent& operator=(ViewportEvent&&) = delete; + + /** + * @brief Canvas size + * + * Note that this method is named "windowSize" to be API compatible with + * Application implementations on other platforms. + * + * Equivalent to @ref framebufferSize(). See @ref Platform-Sdl2Application-dpi + * for more information. + * @see @ref EmscriptenApplication::windowSize() + */ + Vector2i windowSize() const { return _windowSize; } + + #if defined(MAGNUM_TARGET_GL) || defined(DOXYGEN_GENERATING_OUTPUT) + /** + * @brief Framebuffer size + * + * Equivalent to @ref windowSize(). See + * @ref Platform-Sdl2Application-dpi for more information. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + * + * @see @ref EmscriptenApplication::framebufferSize() + */ + Vector2i framebufferSize() const { return _framebufferSize; } + #endif + + /** + * @brief DPI scaling + * + * On some platforms moving an app between displays can result in DPI + * scaling value being changed in tandem with a canvas/framebuffer + * size. Simply resizing a canvas doesn't change the DPI scaling value. + * See @ref Platform-Sdl2Application-dpi for more information. + * @see @ref EmscriptenApplication::dpiScaling() + */ + Vector2 dpiScaling() const { return _dpiScaling; } + + private: + friend EmscriptenApplication; + + explicit ViewportEvent( + const Vector2i& windowSize, + #ifdef MAGNUM_TARGET_GL + const Vector2i& framebufferSize, + #endif + const Vector2& dpiScaling): + _windowSize{windowSize}, + #ifdef MAGNUM_TARGET_GL + _framebufferSize{framebufferSize}, + #endif + _dpiScaling{dpiScaling} {} + + const Vector2i _windowSize; + #ifdef MAGNUM_TARGET_GL + const Vector2i _framebufferSize; + #endif + + const Vector2 _dpiScaling; +}; + +/** +@brief Base for input events + +@see @ref KeyEvent, @ref MouseEvent, @ref MouseMoveEvent, @ref keyPressEvent(), + @ref mousePressEvent(), @ref mouseReleaseEvent(), @ref mouseMoveEvent() +*/ +class EmscriptenApplication::InputEvent { + public: + /** + * @brief Modifier + * + * @see @ref Modifiers, @ref KeyEvent::modifiers(), + * @ref MouseEvent::modifiers() + */ + enum class Modifier: Int { + /** + * Shift + * + * @see @ref KeyEvent::Key::LeftShift, @ref KeyEvent::Key::RightShift + */ + Shift = 1 << 0, + + /** + * Ctrl + * + * @see @ref KeyEvent::Key::LeftCtrl, @ref KeyEvent::Key::RightCtrl + */ + Ctrl = 1 << 1, + + /** + * Alt + * + * @see @ref KeyEvent::Key::LeftAlt, @ref KeyEvent::Key::RightAlt + */ + Alt = 1 << 2, + + /** + * Super key (Windows/⌘) + * + * @see @ref KeyEvent::Key::LeftSuper, @ref KeyEvent::Key::RightSuper + */ + Super = 1 << 3 + }; + + /** + * @brief Set of modifiers + * + * @see @ref KeyEvent::modifiers(), @ref MouseEvent::modifiers(), + * @ref MouseMoveEvent::modifiers() + */ + typedef Containers::EnumSet Modifiers; + + /** @brief Copying is not allowed */ + InputEvent(const InputEvent&) = delete; + + /** @brief Moving is not allowed */ + InputEvent(InputEvent&&) = delete; + + /** @brief Copying is not allowed */ + InputEvent& operator=(const InputEvent&) = delete; + + /** @brief Moving is not allowed */ + InputEvent& operator=(InputEvent&&) = delete; + + /** @copydoc Sdl2Application::InputEvent::setAccepted() */ + void setAccepted(bool accepted = true) { _accepted = accepted; } + + /** @copydoc Sdl2Application::InputEvent::isAccepted() */ + bool isAccepted() const { return _accepted; } + + protected: + explicit InputEvent(): _accepted(false) {} + + ~InputEvent() = default; + + private: + bool _accepted; +}; + +/** +@brief Mouse event + +@see @ref MouseMoveEvent, @ref mousePressEvent(), @ref mouseReleaseEvent() +*/ +class EmscriptenApplication::MouseEvent: public EmscriptenApplication::InputEvent { + friend EmscriptenApplication; + + public: + /** + * @brief Mouse button + * + * @see @ref button() + */ + enum class Button: std::int32_t { + /** Left mouse button */ + Left = 1 << 0, + + /** Middle mouse button */ + Middle = 1 << 1, + + /** Right mouse button */ + Right = 1 << 2 + }; + + /** @brief Button */ + Button button() const; + + /** @brief Position */ + Vector2i position() const; + + /** @brief Modifiers */ + Modifiers modifiers() const; + + private: + explicit MouseEvent(const EmscriptenMouseEvent* event): _event(event) {} + + const EmscriptenMouseEvent* _event; +}; + +/** +@brief Mouse move event + +@see @ref MouseEvent, @ref mouseMoveEvent() +*/ +class EmscriptenApplication::MouseMoveEvent: public EmscriptenApplication::InputEvent { + friend EmscriptenApplication; + + public: + /** + * @brief Mouse button + * + * @see @ref buttons() + */ + enum class Button: std::int32_t { + /** Left mouse button */ + Left = 1 << 0, + + /** Middle mouse button */ + Middle = 1 << 1, + + /** Right mouse button */ + Right = 1 << 2 + }; + + /** + * @brief Set of mouse buttons + * + * @see @ref buttons() + */ + typedef Containers::EnumSet