mirror of https://github.com/mosra/magnum.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
935 lines
39 KiB
935 lines
39 KiB
/* |
|
This file is part of Magnum. |
|
|
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, |
|
2020, 2021 Vladimír Vondruš <mosra@centrum.cz> |
|
Copyright © 2016, 2018 Jonathan Hale <squareys@googlemail.com> |
|
Copyright © 2019 Konstantinos Chatzilygeroudis <costashatz@gmail.com> |
|
Copyright © 2019, 2020 Marco Melorio <m.melorio@icloud.com> |
|
|
|
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 "GlfwApplication.h" |
|
|
|
#include <cstring> |
|
#include <tuple> |
|
#include <Corrade/Containers/Array.h> |
|
#include <Corrade/Containers/StridedArrayView.h> |
|
#include <Corrade/Utility/Arguments.h> |
|
#include <Corrade/Utility/String.h> |
|
#include <Corrade/Utility/Unicode.h> |
|
|
|
#include "Magnum/ImageView.h" |
|
#include "Magnum/PixelFormat.h" |
|
#include "Magnum/Math/ConfigurationValue.h" |
|
#include "Magnum/Platform/ScreenedApplication.hpp" |
|
#include "Magnum/Platform/Implementation/DpiScaling.h" |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
#include "Magnum/GL/Version.h" |
|
#endif |
|
|
|
namespace Magnum { namespace Platform { |
|
|
|
using namespace Containers::Literals; |
|
|
|
#ifdef GLFW_TRUE |
|
/* The docs say that it's the same, verify that just in case */ |
|
static_assert(GLFW_TRUE == true && GLFW_FALSE == false, "GLFW does not have sane bool values"); |
|
#endif |
|
|
|
enum class GlfwApplication::Flag: UnsignedByte { |
|
Redraw = 1 << 0, |
|
TextInputActive = 1 << 1, |
|
Exit = 1 << 2, |
|
#ifdef CORRADE_TARGET_APPLE |
|
HiDpiWarningPrinted = 1 << 3 |
|
#endif |
|
}; |
|
|
|
GlfwApplication::GlfwApplication(const Arguments& arguments): GlfwApplication{arguments, Configuration{}} {} |
|
|
|
GlfwApplication::GlfwApplication(const Arguments& arguments, const Configuration& configuration): GlfwApplication{arguments, NoCreate} { |
|
create(configuration); |
|
} |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
GlfwApplication::GlfwApplication(const Arguments& arguments, const Configuration& configuration, const GLConfiguration& glConfiguration): GlfwApplication{arguments, NoCreate} { |
|
create(configuration, glConfiguration); |
|
} |
|
#endif |
|
|
|
GlfwApplication::GlfwApplication(const Arguments& arguments, NoCreateT): |
|
_flags{Flag::Redraw} |
|
{ |
|
Utility::Arguments args{Implementation::windowScalingArguments()}; |
|
#ifdef MAGNUM_TARGET_GL |
|
_context.emplace(NoCreate, args, arguments.argc, arguments.argv); |
|
#else |
|
/** @todo this is duplicated here and in Sdl2Application, figure out a nice |
|
non-duplicated way to handle this */ |
|
args.addOption("log", "default").setHelp("log", "console logging", "default|quiet|verbose") |
|
.setFromEnvironment("log") |
|
.parse(arguments.argc, arguments.argv); |
|
#endif |
|
|
|
/* Init GLFW */ |
|
glfwSetErrorCallback([](int, const char* const description) { |
|
Error{} << description; |
|
}); |
|
|
|
if(!glfwInit()) { |
|
Error() << "Could not initialize GLFW"; |
|
std::exit(8); |
|
} |
|
|
|
/* Save command-line arguments */ |
|
if(args.value("log") == "verbose") _verboseLog = true; |
|
const std::string dpiScaling = args.value("dpi-scaling"); |
|
if(dpiScaling == "default") |
|
_commandLineDpiScalingPolicy = Implementation::GlfwDpiScalingPolicy::Default; |
|
#ifdef CORRADE_TARGET_APPLE |
|
else if(dpiScaling == "framebuffer") |
|
_commandLineDpiScalingPolicy = Implementation::GlfwDpiScalingPolicy::Framebuffer; |
|
#else |
|
else if(dpiScaling == "virtual") |
|
_commandLineDpiScalingPolicy = Implementation::GlfwDpiScalingPolicy::Virtual; |
|
else if(dpiScaling == "physical") |
|
_commandLineDpiScalingPolicy = Implementation::GlfwDpiScalingPolicy::Physical; |
|
#endif |
|
else if(dpiScaling.find_first_of(" \t\n") != std::string::npos) |
|
_commandLineDpiScaling = args.value<Vector2>("dpi-scaling"); |
|
else |
|
_commandLineDpiScaling = Vector2{args.value<Float>("dpi-scaling")}; |
|
} |
|
|
|
void GlfwApplication::create() { |
|
create(Configuration{}); |
|
} |
|
|
|
void GlfwApplication::create(const Configuration& configuration) { |
|
if(!tryCreate(configuration)) std::exit(1); |
|
} |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
void GlfwApplication::create(const Configuration& configuration, const GLConfiguration& glConfiguration) { |
|
if(!tryCreate(configuration, glConfiguration)) std::exit(1); |
|
} |
|
#endif |
|
|
|
Vector2 GlfwApplication::dpiScaling(const Configuration& configuration) { |
|
std::ostream* verbose = _verboseLog ? Debug::output() : nullptr; |
|
|
|
/* Print a helpful warning in case some extra steps are needed for HiDPI |
|
support */ |
|
#ifdef CORRADE_TARGET_APPLE |
|
if(!Implementation::isAppleBundleHiDpiEnabled() && !(_flags & Flag::HiDpiWarningPrinted)) { |
|
Warning{} << "Platform::GlfwApplication: warning: the executable is not a HiDPI-enabled app bundle"; |
|
_flags |= Flag::HiDpiWarningPrinted; |
|
} |
|
#elif defined(CORRADE_TARGET_WINDOWS) |
|
/** @todo */ |
|
#endif |
|
|
|
/* Use values from the configuration only if not overriden on command line |
|
to something non-default. In any case explicit scaling has a precedence |
|
before the policy. */ |
|
Implementation::GlfwDpiScalingPolicy dpiScalingPolicy{}; |
|
if(!_commandLineDpiScaling.isZero()) { |
|
Debug{verbose} << "Platform::GlfwApplication: user-defined DPI scaling" << _commandLineDpiScaling; |
|
return _commandLineDpiScaling; |
|
} else if(_commandLineDpiScalingPolicy != Implementation::GlfwDpiScalingPolicy::Default) { |
|
dpiScalingPolicy = _commandLineDpiScalingPolicy; |
|
} else if(!configuration.dpiScaling().isZero()) { |
|
Debug{verbose} << "Platform::GlfwApplication: app-defined DPI scaling" << configuration.dpiScaling(); |
|
return configuration.dpiScaling(); |
|
} else { |
|
dpiScalingPolicy = configuration.dpiScalingPolicy(); |
|
} |
|
|
|
/* There's no choice on Apple, it's all controlled by the plist file. So |
|
unless someone specified custom scaling via config or command-line |
|
above, return the default. */ |
|
#ifdef CORRADE_TARGET_APPLE |
|
return Vector2{1.0f}; |
|
|
|
/* Otherwise there's a choice between virtual and physical DPI scaling */ |
|
#else |
|
/* Try to get virtual DPI scaling first, if supported and requested */ |
|
if(dpiScalingPolicy == Implementation::GlfwDpiScalingPolicy::Virtual) { |
|
/* Use Xft.dpi on X11. This could probably be dropped for GLFW 3.3+ |
|
as glfwGetMonitorContentScale() does the same, but I'd still need to |
|
keep it for 2.2 and below, plus the same code needs to be used for |
|
SDL anyway. So keeping it to reduce the chance for unexpected minor |
|
differences across app implementations. */ |
|
#ifdef _MAGNUM_PLATFORM_USE_X11 |
|
const Vector2 dpiScaling{Implementation::x11DpiScaling()}; |
|
if(!dpiScaling.isZero()) { |
|
Debug{verbose} << "Platform::GlfwApplication: virtual DPI scaling" << dpiScaling.x(); |
|
return dpiScaling; |
|
} |
|
|
|
/* Check for DPI awareness on non-RT Windows and then ask for content |
|
scale (available since GLFW 3.3). GLFW is advertising the |
|
application to be DPI-aware on its own even without supplying an |
|
explicit manifest -- https://github.com/glfw/glfw/blob/089ea9af227fdffdf872348923e1c12682e63029/src/win32_init.c#L564-L569 |
|
If, for some reason, the app is still not DPI-aware, tell that to |
|
the user explicitly and don't even attempt to query the value if the |
|
app is not DPI aware. If it's desired to get the DPI value |
|
unconditionally, the user should use physical DPI scaling instead. */ |
|
#elif defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT) |
|
if(!Implementation::isWindowsAppDpiAware()) { |
|
Warning{verbose} << "Platform::GlfwApplication: your application is not set as DPI-aware, DPI scaling won't be used"; |
|
return Vector2{1.0f}; |
|
} |
|
#if GLFW_VERSION_MAJOR*100 + GLFW_VERSION_MINOR >= 303 |
|
GLFWmonitor* const monitor = glfwGetPrimaryMonitor(); |
|
Vector2 dpiScaling; |
|
glfwGetMonitorContentScale(monitor, &dpiScaling.x(), &dpiScaling.y()); |
|
Debug{verbose} << "Platform::GlfwApplication: virtual DPI scaling" << dpiScaling; |
|
return dpiScaling; |
|
#else |
|
Debug{verbose} << "Platform::GlfwApplication: sorry, virtual DPI scaling only available on GLFW 3.3+, falling back to physical DPI scaling"; |
|
#endif |
|
|
|
/* Otherwise ¯\_(ツ)_/¯ */ |
|
#else |
|
Debug{verbose} << "Platform::GlfwApplication: sorry, virtual DPI scaling not implemented on this platform yet, falling back to physical DPI scaling"; |
|
#endif |
|
} |
|
|
|
/* At this point, either the virtual DPI query failed or a physical DPI |
|
scaling is requested */ |
|
CORRADE_INTERNAL_ASSERT(dpiScalingPolicy == Implementation::GlfwDpiScalingPolicy::Virtual || dpiScalingPolicy == Implementation::GlfwDpiScalingPolicy::Physical); |
|
|
|
/* Physical DPI scaling. Enable only on Linux (where it gets the usually |
|
very-off value from X11) and on non-RT Windows (where it calculates it |
|
from actual monitor dimensions). */ |
|
#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) |
|
GLFWmonitor* const monitor = glfwGetPrimaryMonitor(); |
|
const GLFWvidmode* const mode = glfwGetVideoMode(monitor); |
|
Vector2i monitorSize; |
|
glfwGetMonitorPhysicalSize(monitor, &monitorSize.x(), &monitorSize.y()); |
|
if(monitorSize.isZero()) { |
|
Warning{verbose} << "Platform::GlfwApplication: the physical monitor size is zero? DPI scaling won't be used"; |
|
return Vector2{1.0f}; |
|
} |
|
auto dpi = Vector2{Vector2i{mode->width, mode->height}*25.4f/Vector2{monitorSize}}; |
|
const Vector2 dpiScaling{dpi/96.0f}; |
|
Debug{verbose} << "Platform::GlfwApplication: physical DPI scaling" << dpiScaling; |
|
return dpiScaling; |
|
|
|
/* Not implemented otherwise */ |
|
#else |
|
Debug{verbose} << "Platform::GlfwApplication: sorry, physical DPI scaling not implemented on this platform yet"; |
|
return Vector2{1.0f}; |
|
#endif |
|
#endif |
|
} |
|
|
|
void GlfwApplication::setWindowTitle(const std::string& title) { |
|
glfwSetWindowTitle(_window, title.data()); |
|
} |
|
|
|
#if GLFW_VERSION_MAJOR*100 + GLFW_VERSION_MINOR >= 302 |
|
void GlfwApplication::setWindowIcon(const ImageView2D& image) { |
|
setWindowIcon({&image, 1}); |
|
} |
|
|
|
namespace { |
|
|
|
template<class T> inline void packPixels(const Containers::StridedArrayView2D<const T>& in, const Containers::StridedArrayView2D<Color4ub>& out) { |
|
for(std::size_t row = 0; row != in.size()[0]; ++row) |
|
for(std::size_t col = 0; col != in.size()[1]; ++col) |
|
out[row][col] = in[row][col]; |
|
} |
|
|
|
} |
|
|
|
void GlfwApplication::setWindowIcon(const Containers::ArrayView<const ImageView2D> images) { |
|
/* Calculate the total size needed to allocate first so we don't allocate |
|
a ton of tiny arrays */ |
|
std::size_t size = 0; |
|
for(const ImageView2D& image: images) |
|
size += sizeof(GLFWimage) + 4*image.size().product(); |
|
Containers::Array<char> data{size}; |
|
|
|
/* Pack array of GLFWimages and pixel data together into the memory |
|
allocated above */ |
|
std::size_t offset = images.size()*sizeof(GLFWimage); |
|
Containers::ArrayView<GLFWimage> glfwImages = Containers::arrayCast<GLFWimage>(data.prefix(offset)); |
|
std::size_t i = 0; |
|
for(const ImageView2D& image: images) { |
|
/* Copy and tightly pack pixels. GLFW doesn't allow arbitrary formats |
|
or strides (for subimages and/or Y flip), so we have to copy */ |
|
Containers::ArrayView<char> target = data.slice(offset, offset + 4*image.size().product()); |
|
auto out = Containers::StridedArrayView2D<Color4ub>{ |
|
Containers::arrayCast<Color4ub>(target), |
|
{std::size_t(image.size().y()), |
|
std::size_t(image.size().x())}}.flipped<0>(); |
|
/** @todo handle sRGB differently? */ |
|
if(image.format() == PixelFormat::RGB8Snorm || |
|
image.format() == PixelFormat::RGB8Unorm) |
|
packPixels(image.pixels<Color3ub>(), out); |
|
else if(image.format() == PixelFormat::RGBA8Snorm || |
|
image.format() == PixelFormat::RGBA8Unorm) |
|
packPixels(image.pixels<Color4ub>(), out); |
|
else CORRADE_ASSERT_UNREACHABLE("Platform::GlfwApplication::setWindowIcon(): unexpected format" << image.format(), ); |
|
|
|
/* Specify the image metadata */ |
|
glfwImages[i].width = image.size().x(); |
|
glfwImages[i].height = image.size().y(); |
|
glfwImages[i].pixels = reinterpret_cast<unsigned char*>(target.data()); |
|
|
|
++i; |
|
offset += target.size(); |
|
} |
|
|
|
glfwSetWindowIcon(_window, glfwImages.size(), glfwImages); |
|
} |
|
|
|
void GlfwApplication::setWindowIcon(std::initializer_list<ImageView2D> images) { |
|
setWindowIcon(Containers::arrayView(images)); |
|
} |
|
#endif |
|
|
|
bool GlfwApplication::tryCreate(const Configuration& configuration) { |
|
#ifdef MAGNUM_TARGET_GL |
|
#ifdef GLFW_NO_API |
|
if(!(configuration.windowFlags() & Configuration::WindowFlag::Contextless)) |
|
#endif |
|
{ |
|
return tryCreate(configuration, GLConfiguration{}); |
|
} |
|
#endif |
|
|
|
CORRADE_ASSERT(!_window, "Platform::GlfwApplication::tryCreate(): window already created", false); |
|
|
|
/* Scale window based on DPI */ |
|
_dpiScaling = dpiScaling(configuration); |
|
const Vector2i scaledWindowSize = configuration.size()*_dpiScaling; |
|
|
|
/* Window flags */ |
|
GLFWmonitor* monitor = nullptr; /* Needed for setting fullscreen */ |
|
if (configuration.windowFlags() >= Configuration::WindowFlag::Fullscreen) { |
|
monitor = glfwGetPrimaryMonitor(); |
|
glfwWindowHint(GLFW_AUTO_ICONIFY, configuration.windowFlags() >= Configuration::WindowFlag::AutoIconify); |
|
} else { |
|
const Configuration::WindowFlags& flags = configuration.windowFlags(); |
|
glfwWindowHint(GLFW_DECORATED, !(flags >= Configuration::WindowFlag::Borderless)); |
|
glfwWindowHint(GLFW_RESIZABLE, flags >= Configuration::WindowFlag::Resizable); |
|
glfwWindowHint(GLFW_VISIBLE, !(flags >= Configuration::WindowFlag::Hidden)); |
|
#ifdef GLFW_MAXIMIZED |
|
glfwWindowHint(GLFW_MAXIMIZED, flags >= Configuration::WindowFlag::Maximized); |
|
#endif |
|
glfwWindowHint(GLFW_FLOATING, flags >= Configuration::WindowFlag::AlwaysOnTop); |
|
} |
|
glfwWindowHint(GLFW_FOCUSED, configuration.windowFlags() >= Configuration::WindowFlag::Focused); |
|
|
|
#ifdef GLFW_NO_API |
|
/* Disable implicit GL context creation */ |
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); |
|
#endif |
|
|
|
/* Create the window */ |
|
_window = glfwCreateWindow(scaledWindowSize.x(), scaledWindowSize.y(), configuration.title().c_str(), monitor, nullptr); |
|
if(!_window) { |
|
Error() << "Platform::GlfwApplication::tryCreate(): cannot create window"; |
|
glfwTerminate(); |
|
return false; |
|
} |
|
|
|
/* Proceed with configuring other stuff that couldn't be done with window |
|
hints */ |
|
if(configuration.windowFlags() >= Configuration::WindowFlag::Minimized) |
|
glfwIconifyWindow(_window); |
|
#ifdef MAGNUM_BUILD_DEPRECATED |
|
CORRADE_IGNORE_DEPRECATED_PUSH |
|
glfwSetInputMode(_window, GLFW_CURSOR, Int(configuration.cursorMode())); |
|
CORRADE_IGNORE_DEPRECATED_POP |
|
#endif |
|
|
|
return true; |
|
} |
|
|
|
namespace { |
|
|
|
GlfwApplication::InputEvent::Modifiers currentGlfwModifiers(GLFWwindow* window) { |
|
static_assert(GLFW_PRESS == true && GLFW_RELEASE == false, |
|
"GLFW press and release constants do not correspond to bool values"); |
|
|
|
GlfwApplication::InputEvent::Modifiers mods; |
|
if(glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) || |
|
glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT)) |
|
mods |= GlfwApplication::InputEvent::Modifier::Shift; |
|
if(glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) || |
|
glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL)) |
|
mods |= GlfwApplication::InputEvent::Modifier::Ctrl; |
|
if(glfwGetKey(window, GLFW_KEY_LEFT_ALT) || |
|
glfwGetKey(window, GLFW_KEY_RIGHT_ALT)) |
|
mods |= GlfwApplication::InputEvent::Modifier::Alt; |
|
if(glfwGetKey(window, GLFW_KEY_LEFT_SUPER) || |
|
glfwGetKey(window, GLFW_KEY_RIGHT_SUPER)) |
|
mods |= GlfwApplication::InputEvent::Modifier::Super; |
|
|
|
return mods; |
|
} |
|
|
|
} |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
bool GlfwApplication::tryCreate(const Configuration& configuration, const GLConfiguration& glConfiguration) { |
|
CORRADE_ASSERT(!_window && _context->version() == GL::Version::None, "Platform::GlfwApplication::tryCreate(): window with OpenGL context already created", false); |
|
|
|
/* Scale window based on DPI */ |
|
_dpiScaling = dpiScaling(configuration); |
|
const Vector2i scaledWindowSize = configuration.size()*_dpiScaling; |
|
|
|
/* Window flags */ |
|
GLFWmonitor* monitor = nullptr; /* Needed for setting fullscreen */ |
|
if (configuration.windowFlags() >= Configuration::WindowFlag::Fullscreen) { |
|
monitor = glfwGetPrimaryMonitor(); |
|
glfwWindowHint(GLFW_AUTO_ICONIFY, configuration.windowFlags() >= Configuration::WindowFlag::AutoIconify); |
|
} else { |
|
const Configuration::WindowFlags& flags = configuration.windowFlags(); |
|
glfwWindowHint(GLFW_DECORATED, !(flags >= Configuration::WindowFlag::Borderless)); |
|
glfwWindowHint(GLFW_RESIZABLE, flags >= Configuration::WindowFlag::Resizable); |
|
glfwWindowHint(GLFW_VISIBLE, !(flags >= Configuration::WindowFlag::Hidden)); |
|
#ifdef GLFW_MAXIMIZED |
|
glfwWindowHint(GLFW_MAXIMIZED, flags >= Configuration::WindowFlag::Maximized); |
|
#endif |
|
glfwWindowHint(GLFW_FLOATING, flags >= Configuration::WindowFlag::AlwaysOnTop); |
|
} |
|
glfwWindowHint(GLFW_FOCUSED, configuration.windowFlags() >= Configuration::WindowFlag::Focused); |
|
|
|
/* Framebuffer setup */ |
|
glfwWindowHint(GLFW_RED_BITS, glConfiguration.colorBufferSize().r()); |
|
glfwWindowHint(GLFW_GREEN_BITS, glConfiguration.colorBufferSize().g()); |
|
glfwWindowHint(GLFW_BLUE_BITS, glConfiguration.colorBufferSize().b()); |
|
glfwWindowHint(GLFW_ALPHA_BITS, glConfiguration.colorBufferSize().a()); |
|
glfwWindowHint(GLFW_DEPTH_BITS, glConfiguration.depthBufferSize()); |
|
glfwWindowHint(GLFW_STENCIL_BITS, glConfiguration.stencilBufferSize()); |
|
glfwWindowHint(GLFW_SAMPLES, glConfiguration.sampleCount()); |
|
glfwWindowHint(GLFW_SRGB_CAPABLE, glConfiguration.isSrgbCapable()); |
|
|
|
/* Request debug context if GpuValidation is enabled either via the |
|
configuration or via command-line */ |
|
GLConfiguration::Flags glFlags = glConfiguration.flags(); |
|
if((glFlags & GLConfiguration::Flag::GpuValidation) || (_context->configurationFlags() & GL::Context::Configuration::Flag::GpuValidation)) |
|
glFlags |= GLConfiguration::Flag::Debug; |
|
|
|
#ifdef GLFW_CONTEXT_NO_ERROR |
|
glfwWindowHint(GLFW_CONTEXT_NO_ERROR, glFlags >= GLConfiguration::Flag::NoError); |
|
#endif |
|
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, glFlags >= GLConfiguration::Flag::Debug); |
|
glfwWindowHint(GLFW_STEREO, glFlags >= GLConfiguration::Flag::Stereo); |
|
|
|
/* Set context version, if requested */ |
|
if(glConfiguration.version() != GL::Version::None) { |
|
Int major, minor; |
|
std::tie(major, minor) = version(glConfiguration.version()); |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, major); |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minor); |
|
#ifndef MAGNUM_TARGET_GLES |
|
if(glConfiguration.version() >= GL::Version::GL320) { |
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); |
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, glFlags >= GLConfiguration::Flag::ForwardCompatible); |
|
} |
|
#else |
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); |
|
/* Force EGL on Windows and non-desktop GLES -- needed by ANGLE: |
|
https://stackoverflow.com/a/58904181/4084782 . Might be useful on |
|
other platforms as well (Mac?), not tested yet. */ |
|
#if defined(CORRADE_TARGET_WINDOWS) && !defined(MAGNUM_TARGET_DESKTOP_GLES) |
|
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); |
|
#endif |
|
#endif |
|
|
|
/* Request usable version otherwise */ |
|
} else { |
|
#ifndef MAGNUM_TARGET_GLES |
|
/* First try to create core context. This is needed mainly on macOS and |
|
Mesa, as support for recent OpenGL versions isn't implemented in |
|
compatibility contexts (which are the default). Unlike SDL2, GLFW |
|
requires at least version 3.2 to be able to request a core profile. */ |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); |
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); |
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, glFlags >= GLConfiguration::Flag::ForwardCompatible); |
|
#else |
|
/* For ES the major context version is compile-time constant */ |
|
#ifdef MAGNUM_TARGET_GLES3 |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); |
|
#elif defined(MAGNUM_TARGET_GLES2) |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); |
|
#else |
|
#error unsupported OpenGL ES version |
|
#endif |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); |
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); |
|
/* Force EGL on Windows and non-desktop GLES -- needed by ANGLE: |
|
https://stackoverflow.com/a/58904181/4084782 . Might be useful on |
|
other platforms as well (Mac?), not tested yet. */ |
|
#if defined(CORRADE_TARGET_WINDOWS) && !defined(MAGNUM_TARGET_DESKTOP_GLES) |
|
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); |
|
#endif |
|
#endif |
|
} |
|
|
|
/* Create window. Hide it by default so we don't have distracting window |
|
blinking in case we have to destroy it again right away. If the creation |
|
succeeds, make the context current so we can query GL_VENDOR below. |
|
If we are on Wayland, this is causing a segfault; a blinking window is |
|
acceptable in this case. */ |
|
constexpr const char waylandString[] = "wayland"; |
|
const char* const xdgSessionType = std::getenv("XDG_SESSION_TYPE"); |
|
if(!xdgSessionType || std::strncmp(xdgSessionType, waylandString, sizeof(waylandString)) != 0) |
|
glfwWindowHint(GLFW_VISIBLE, false); |
|
else if(_verboseLog) |
|
Warning{} << "Platform::GlfwApplication: Wayland detected, GL context has to be created with the window visible and may cause flicker on startup"; |
|
if((_window = glfwCreateWindow(scaledWindowSize.x(), scaledWindowSize.y(), configuration.title().c_str(), monitor, nullptr))) |
|
glfwMakeContextCurrent(_window); |
|
|
|
#ifndef MAGNUM_TARGET_GLES |
|
/* Fall back to (forward compatible) GL 2.1, if version is not |
|
user-specified and either core context creation fails or we are on |
|
binary NVidia/AMD drivers on Linux/Windows or Intel Windows drivers. |
|
Instead of creating forward-compatible context with highest available |
|
version, they force the version to the one specified, which is |
|
completely useless behavior. */ |
|
#ifndef CORRADE_TARGET_APPLE |
|
constexpr static const char nvidiaVendorString[] = "NVIDIA Corporation"; |
|
#ifdef CORRADE_TARGET_WINDOWS |
|
constexpr static const char intelVendorString[] = "Intel"; |
|
#endif |
|
constexpr static const char amdVendorString[] = "ATI Technologies Inc."; |
|
const char* vendorString; |
|
#endif |
|
if(glConfiguration.version() == GL::Version::None && (!_window |
|
#ifndef CORRADE_TARGET_APPLE |
|
/* If context creation fails *really bad*, glGetString() may actually |
|
return nullptr. Check for that to avoid crashes deep inside |
|
strncmp(). Sorry about the UGLY code, HOPEFULLY THERE WON'T BE MORE |
|
WORKAROUNDS */ |
|
|| (vendorString = reinterpret_cast<const char*>(glGetString(GL_VENDOR)), |
|
vendorString && (std::strncmp(vendorString, nvidiaVendorString, sizeof(nvidiaVendorString)) == 0 || |
|
#ifdef CORRADE_TARGET_WINDOWS |
|
std::strncmp(vendorString, intelVendorString, sizeof(intelVendorString)) == 0 || |
|
#endif |
|
std::strncmp(vendorString, amdVendorString, sizeof(amdVendorString)) == 0) |
|
&& !_context->isDriverWorkaroundDisabled("no-forward-compatible-core-context"_s)) |
|
#endif |
|
)) { |
|
/* Don't print any warning when doing the workaround, because the bug |
|
will be there probably forever */ |
|
if(!_window) Warning{} |
|
<< "Platform::GlfwApplication::tryCreate(): cannot create a window with core OpenGL context, falling back to compatibility context"; |
|
else glfwDestroyWindow(_window); |
|
|
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); |
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); |
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_ANY_PROFILE); |
|
/* Discard the ForwardCompatible flag for the fallback. Having it set |
|
makes the fallback context creation fail on Mesa's Zink (which is |
|
just 2.1) and I assume on others as well. */ |
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, false); |
|
|
|
_window = glfwCreateWindow(scaledWindowSize.x(), scaledWindowSize.y(), configuration.title().c_str(), monitor, nullptr); |
|
} |
|
#endif |
|
|
|
if(!_window) { |
|
Error() << "Platform::GlfwApplication::tryCreate(): cannot create a window with OpenGL context"; |
|
return false; |
|
} |
|
|
|
/* Proceed with configuring other stuff that couldn't be done with window |
|
hints */ |
|
if(configuration.windowFlags() >= Configuration::WindowFlag::Minimized) |
|
glfwIconifyWindow(_window); |
|
#ifdef MAGNUM_BUILD_DEPRECATED |
|
CORRADE_IGNORE_DEPRECATED_PUSH |
|
glfwSetInputMode(_window, GLFW_CURSOR, Int(configuration.cursorMode())); |
|
CORRADE_IGNORE_DEPRECATED_POP |
|
#endif |
|
|
|
/* If exit() was called before the window got created, be sure to propagate |
|
it */ |
|
glfwSetWindowShouldClose(_window, !!(_flags & Flag::Exit)); |
|
|
|
/* Make the final context current */ |
|
glfwMakeContextCurrent(_window); |
|
|
|
/* Destroy everything when the Magnum context creation fails */ |
|
if(!_context->tryCreate(glConfiguration)) { |
|
glfwDestroyWindow(_window); |
|
_window = nullptr; |
|
} |
|
|
|
/* Show the window once we are sure that everything is okay */ |
|
if(!(configuration.windowFlags() & Configuration::WindowFlag::Hidden)) |
|
glfwShowWindow(_window); |
|
|
|
/* Return true if the initialization succeeds */ |
|
return true; |
|
} |
|
#endif |
|
|
|
void GlfwApplication::setupCallbacks() { |
|
glfwSetWindowUserPointer(_window, this); |
|
glfwSetWindowCloseCallback(_window, [](GLFWwindow* const window){ |
|
ExitEvent e; |
|
static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window))->exitEvent(e); |
|
if(!e.isAccepted()) glfwSetWindowShouldClose(window, false); |
|
}); |
|
glfwSetWindowRefreshCallback(_window, [](GLFWwindow* const window){ |
|
/* Properly redraw after the window is restored from minimized state */ |
|
static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window))->drawEvent(); |
|
}); |
|
#ifdef MAGNUM_TARGET_GL |
|
glfwSetFramebufferSizeCallback |
|
#else |
|
glfwSetWindowSizeCallback |
|
#endif |
|
(_window, [](GLFWwindow* const window, const int w, const int h) { |
|
auto& app = *static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window)); |
|
#ifdef MAGNUM_TARGET_GL |
|
ViewportEvent e{app.windowSize(), {w, h}, app.dpiScaling()}; |
|
#else |
|
ViewportEvent e{{w, h}, app.dpiScaling()}; |
|
#endif |
|
app.viewportEvent(e); |
|
}); |
|
glfwSetKeyCallback(_window, [](GLFWwindow* const window, const int key, int, const int action, const int mods) { |
|
auto& app = *static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window)); |
|
|
|
KeyEvent e(static_cast<KeyEvent::Key>(key), {static_cast<InputEvent::Modifier>(mods)}, action == GLFW_REPEAT); |
|
|
|
if(action == GLFW_PRESS || action == GLFW_REPEAT) |
|
app.keyPressEvent(e); |
|
else if(action == GLFW_RELEASE) |
|
app.keyReleaseEvent(e); |
|
}); |
|
glfwSetMouseButtonCallback(_window, [](GLFWwindow* const window, const int button, const int action, const int mods) { |
|
auto& app = *static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window)); |
|
|
|
double x, y; |
|
glfwGetCursorPos(window, &x, &y); |
|
MouseEvent e(static_cast<MouseEvent::Button>(button), {Int(x), Int(y)}, {static_cast<InputEvent::Modifier>(mods)}); |
|
|
|
if(action == GLFW_PRESS) /* we don't handle GLFW_REPEAT */ |
|
app.mousePressEvent(e); |
|
else if(action == GLFW_RELEASE) |
|
app.mouseReleaseEvent(e); |
|
}); |
|
glfwSetCursorPosCallback(_window, [](GLFWwindow* const window, const double x, const double y) { |
|
auto& app = *static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window)); |
|
/* Avoid bogus offset at first -- report 0 when the event is called for |
|
the first time */ |
|
Vector2i position{Int(x), Int(y)}; |
|
MouseMoveEvent e{window, position, |
|
app._previousMouseMovePosition == Vector2i{-1} ? Vector2i{} : |
|
position - app._previousMouseMovePosition}; |
|
app._previousMouseMovePosition = position; |
|
app.mouseMoveEvent(e); |
|
}); |
|
glfwSetScrollCallback(_window, [](GLFWwindow* window, double xoffset, double yoffset) { |
|
MouseScrollEvent e(window, Vector2{Float(xoffset), Float(yoffset)}); |
|
static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window))->mouseScrollEvent(e); |
|
}); |
|
glfwSetCharCallback(_window, [](GLFWwindow* window, unsigned int codepoint) { |
|
auto& app = *static_cast<GlfwApplication*>(glfwGetWindowUserPointer(window)); |
|
|
|
if(!(app._flags & Flag::TextInputActive)) return; |
|
|
|
char utf8[4]{}; |
|
const std::size_t size = Utility::Unicode::utf8(codepoint, utf8); |
|
TextInputEvent e{{utf8, size}}; |
|
app.textInputEvent(e); |
|
}); |
|
} |
|
|
|
GlfwApplication::~GlfwApplication() { |
|
#ifdef MAGNUM_TARGET_GL |
|
/* Destroy Magnum context first to avoid it potentially accessing the |
|
now-destroyed GL context after */ |
|
_context = Containers::NullOpt; |
|
#endif |
|
|
|
glfwDestroyWindow(_window); |
|
for(auto& cursor: _cursors) |
|
glfwDestroyCursor(cursor); |
|
glfwTerminate(); |
|
} |
|
|
|
Vector2i GlfwApplication::windowSize() const { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::windowSize(): no window opened", {}); |
|
|
|
Vector2i size; |
|
glfwGetWindowSize(_window, &size.x(), &size.y()); |
|
return size; |
|
} |
|
|
|
void GlfwApplication::setWindowSize(const Vector2i& size) { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::setWindowSize(): no window opened", ); |
|
|
|
const Vector2i newSize = _dpiScaling*size; |
|
glfwSetWindowSize(_window, newSize.x(), newSize.y()); |
|
} |
|
|
|
#if GLFW_VERSION_MAJOR*100 + GLFW_VERSION_MINOR >= 302 |
|
void GlfwApplication::setMinWindowSize(const Vector2i& size) { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::setMinWindowSize(): no window opened", ); |
|
|
|
const Vector2i newSize = _dpiScaling*size; |
|
glfwSetWindowSizeLimits(_window, newSize.x(), newSize.y(), _maxWindowSize.x(), _maxWindowSize.y()); |
|
_minWindowSize = newSize; |
|
} |
|
|
|
void GlfwApplication::setMaxWindowSize(const Vector2i& size) { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::setMaxWindowSize(): no window opened", ); |
|
|
|
const Vector2i newSize = _dpiScaling*size; |
|
glfwSetWindowSizeLimits(_window, _minWindowSize.x(), _minWindowSize.y(), newSize.x(), newSize.y()); |
|
_maxWindowSize = newSize; |
|
} |
|
#endif |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
Vector2i GlfwApplication::framebufferSize() const { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::framebufferSize(): no window opened", {}); |
|
|
|
Vector2i size; |
|
glfwGetFramebufferSize(_window, &size.x(), &size.y()); |
|
return size; |
|
} |
|
#endif |
|
|
|
void GlfwApplication::setSwapInterval(const Int interval) { |
|
glfwSwapInterval(interval); |
|
} |
|
|
|
void GlfwApplication::redraw() { _flags |= Flag::Redraw; } |
|
|
|
int GlfwApplication::exec() { |
|
while(mainLoopIteration()) {} |
|
|
|
return _exitCode; |
|
} |
|
|
|
bool GlfwApplication::mainLoopIteration() { |
|
/* If exit was requested directly in the constructor, exit immediately |
|
without calling anything else */ |
|
if(_flags & Flag::Exit || glfwWindowShouldClose(_window)) return false; |
|
|
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::mainLoopIteration(): no window opened", {}); |
|
|
|
/* |
|
If callbacks are not set up yet, do that. Can't be done inside |
|
tryCreate() because: |
|
|
|
1. On Windows, GLFW fires a viewport event already when creating the |
|
window, which means viewportEvent() gets called even before the |
|
constructor exits. That's not a problem if the window is created |
|
implicitly (because derived class vtable is not setup yet and so |
|
the call goes into the base class no-op viewportEvent()), but when |
|
calling create() / tryCreate() from user constructor, this might |
|
lead to crashes as things touched by viewportEvent() might not be |
|
initialized yet. |
|
2. On macOS, GLFW might sometimes (hard to reproduce) trigger a draw |
|
event when creating the window. That's even worse than on Windows |
|
because this leads to pure virtual drawEvent() getting called and |
|
the application aborting due to a pure virtual method call in case |
|
GL context is created implicitly by the base class constructor (at |
|
which point the vtable pointers for the derived class are not set |
|
up yet). |
|
*/ |
|
if(glfwGetWindowUserPointer(_window) != this) setupCallbacks(); |
|
|
|
/* If redrawing, poll for events immediately after drawEvent() (which could |
|
be setting the Redraw flag again, thus doing constant redraw). If not, |
|
avoid spinning the CPU by waiting for the next input event. */ |
|
if(_flags & Flag::Redraw) { |
|
_flags &= ~Flag::Redraw; |
|
drawEvent(); |
|
glfwPollEvents(); |
|
} else glfwWaitEvents(); |
|
|
|
return !glfwWindowShouldClose(_window); |
|
} |
|
|
|
void GlfwApplication::exit(int exitCode) { |
|
_flags |= Flag::Exit; |
|
_exitCode = exitCode; |
|
|
|
/* If the window is already created, tell GLFW that it should close. If |
|
not, this is done in tryCreate() once the window is created */ |
|
if(_window) glfwSetWindowShouldClose(_window, true); |
|
} |
|
|
|
namespace { |
|
|
|
constexpr Int CursorMap[] { |
|
GLFW_ARROW_CURSOR, |
|
GLFW_IBEAM_CURSOR, |
|
GLFW_CROSSHAIR_CURSOR, |
|
#ifdef GLFW_RESIZE_NWSE_CURSOR |
|
GLFW_RESIZE_NWSE_CURSOR, |
|
GLFW_RESIZE_NESW_CURSOR, |
|
#endif |
|
GLFW_HRESIZE_CURSOR, |
|
GLFW_VRESIZE_CURSOR, |
|
#ifdef GLFW_RESIZE_NWSE_CURSOR |
|
GLFW_RESIZE_ALL_CURSOR, |
|
GLFW_NOT_ALLOWED_CURSOR, |
|
#endif |
|
GLFW_HAND_CURSOR |
|
}; |
|
|
|
} |
|
|
|
void GlfwApplication::setCursor(Cursor cursor) { |
|
CORRADE_ASSERT(_window, "Platform::GlfwApplication::setCursor(): no window opened", ); |
|
|
|
_cursor = cursor; |
|
|
|
if(cursor == Cursor::Hidden) { |
|
glfwSetInputMode(_window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); |
|
return; |
|
} else if(cursor == Cursor::HiddenLocked) { |
|
glfwSetInputMode(_window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); |
|
return; |
|
} else { |
|
glfwSetInputMode(_window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); |
|
} |
|
|
|
/* The second condition could be a static assert but it doesn't let me |
|
because "this pointer only accessible in a constexpr function". Thanks |
|
for nothing, C++. */ |
|
CORRADE_INTERNAL_ASSERT(UnsignedInt(cursor) < Containers::arraySize(_cursors) && Containers::arraySize(_cursors) == Containers::arraySize(CursorMap)); |
|
|
|
if(!_cursors[UnsignedInt(cursor)]) |
|
_cursors[UnsignedInt(cursor)] = glfwCreateStandardCursor(CursorMap[UnsignedInt(cursor)]); |
|
|
|
glfwSetCursor(_window, _cursors[UnsignedInt(cursor)]); |
|
} |
|
|
|
GlfwApplication::Cursor GlfwApplication::cursor() { |
|
return _cursor; |
|
} |
|
|
|
auto GlfwApplication::MouseMoveEvent::buttons() -> Buttons { |
|
if(!_buttons) { |
|
_buttons = Buttons{}; |
|
for(const Int button: {GLFW_MOUSE_BUTTON_LEFT, |
|
GLFW_MOUSE_BUTTON_MIDDLE, |
|
GLFW_MOUSE_BUTTON_RIGHT}) { |
|
if(glfwGetMouseButton(_window, button) == GLFW_PRESS) |
|
*_buttons |= Button(1 << button); |
|
} |
|
} |
|
|
|
return *_buttons; |
|
} |
|
|
|
auto GlfwApplication::MouseMoveEvent::modifiers() -> Modifiers { |
|
if(!_modifiers) _modifiers = currentGlfwModifiers(_window); |
|
return *_modifiers; |
|
} |
|
|
|
Vector2i GlfwApplication::MouseScrollEvent::position() { |
|
if(!_position) { |
|
Vector2d position; |
|
glfwGetCursorPos(_window, &position.x(), &position.y()); |
|
_position = Vector2i{position}; |
|
} |
|
|
|
return *_position; |
|
} |
|
|
|
auto GlfwApplication::MouseScrollEvent::modifiers() -> Modifiers { |
|
if(!_modifiers) _modifiers = currentGlfwModifiers(_window); |
|
return *_modifiers; |
|
} |
|
|
|
void GlfwApplication::exitEvent(ExitEvent& event) { |
|
event.setAccepted(); |
|
} |
|
|
|
void GlfwApplication::viewportEvent(ViewportEvent& event) { |
|
static_cast<void>(event); |
|
} |
|
|
|
void GlfwApplication::keyPressEvent(KeyEvent&) {} |
|
void GlfwApplication::keyReleaseEvent(KeyEvent&) {} |
|
void GlfwApplication::mousePressEvent(MouseEvent&) {} |
|
void GlfwApplication::mouseReleaseEvent(MouseEvent&) {} |
|
void GlfwApplication::mouseMoveEvent(MouseMoveEvent&) {} |
|
void GlfwApplication::mouseScrollEvent(MouseScrollEvent&) {} |
|
void GlfwApplication::textInputEvent(TextInputEvent&) {} |
|
|
|
bool GlfwApplication::isTextInputActive() const { |
|
return !!(_flags & Flag::TextInputActive); |
|
} |
|
|
|
void GlfwApplication::startTextInput() { |
|
_flags |= Flag::TextInputActive; |
|
} |
|
|
|
void GlfwApplication::stopTextInput() { |
|
_flags &= ~Flag::TextInputActive; |
|
} |
|
|
|
#ifdef MAGNUM_TARGET_GL |
|
GlfwApplication::GLConfiguration::GLConfiguration(): |
|
_colorBufferSize{8, 8, 8, 8}, _depthBufferSize{24}, _stencilBufferSize{0}, |
|
_sampleCount{0}, _version{GL::Version::None}, _srgbCapable{false} |
|
{ |
|
#ifndef MAGNUM_TARGET_GLES |
|
addFlags(Flag::ForwardCompatible); |
|
#endif |
|
} |
|
|
|
GlfwApplication::GLConfiguration::~GLConfiguration() = default; |
|
#endif |
|
|
|
GlfwApplication::Configuration::Configuration(): |
|
_title{"Magnum GLFW Application"}, |
|
_size{800, 600}, |
|
_windowFlags{WindowFlag::Focused}, |
|
_dpiScalingPolicy{DpiScalingPolicy::Default} {} |
|
|
|
GlfwApplication::Configuration::~Configuration() = default; |
|
|
|
#if defined(DOXYGEN_GENERATING_OUTPUT) || GLFW_VERSION_MAJOR*100 + GLFW_VERSION_MINOR >= 302 |
|
std::string GlfwApplication::KeyEvent::keyName(const Key key) { |
|
/* It can return null, so beware */ |
|
return Utility::String::fromArray(glfwGetKeyName(int(key), 0)); |
|
} |
|
|
|
std::string GlfwApplication::KeyEvent::keyName() const { |
|
return keyName(_key); |
|
} |
|
#endif |
|
|
|
template class BasicScreen<GlfwApplication>; |
|
template class BasicScreenedApplication<GlfwApplication>; |
|
|
|
}}
|
|
|