Files
imhex/main/gui/source/window/win_window.cpp
paxcut 5c4cf7379f feat: Added Subpixel Font rendering (#2092)
Proof of concept for implementing subpixel processing in ImGui. This is
work in progress, and it is bound to have problems.

What it does:
1) Uses freetype own subpixel processing implementation to build a
32-bit color atlas for the default font only (no icons, no unifont) . 2)
Avoids pixel perfect font conversion when possible. 3) Self contained,
no ImGui source code changes.
4) Results in much improved legibility of fonts rendered on low dpi LCD
screens that use horizontal RGB pixel layouts (no BRG or OLED or CRT if
they even exist anymore)

What it doesn't:
1) Fancy class based interface. The code is barely the minimum needed to
show it can work. 2) Dual source color blending. That needs to be
implemented in shader code, so it needs to change ImGui source code
although minimally. This will result in some characters appearing dimmer
than others. Easily fixed with small fragment and vertex shaders. 3)
subpixel positioning. If characters are very thin they will look
colored, or they can be moved to improve legibility. 4) deal with
detection of fringe cases including rare pixel layouts, non LCD screens,
Mac-OS not handling subpixel rendering and any other deviation from the
standard LCD. 5) tries to be efficient in speed or memory use. Font
Atlases will be 4 times the size they were before, but there are no
noticeable delays in font loading in the examples I have tried.

Any comments and code improvements are welcome.

---------

Co-authored-by: Nik <werwolv98@gmail.com>
2025-05-11 15:36:32 +02:00

691 lines
25 KiB
C++

#include "window.hpp"
#if defined(OS_WINDOWS)
#include "messaging.hpp"
#include <hex/api/content_registry.hpp>
#include <hex/api/theme_manager.hpp>
#include <hex/helpers/utils.hpp>
#include <hex/helpers/logger.hpp>
#include <hex/helpers/default_paths.hpp>
#include <hex/api/events/events_gui.hpp>
#include <hex/api/events/events_lifecycle.hpp>
#include <hex/api/events/events_interaction.hpp>
#include <hex/api/events/requests_gui.hpp>
#include <imgui.h>
#include <imgui_internal.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#undef GLFW_EXPOSE_NATIVE_WIN32
#include <winbase.h>
#include <winuser.h>
#include <dwmapi.h>
#include <windowsx.h>
#include <shobjidl.h>
#include <wrl/client.h>
#include <fcntl.h>
#include <shellapi.h>
#include <timeapi.h>
#include <VersionHelpers.h>
#include <cstdio>
#if !defined(STDIN_FILENO)
#define STDIN_FILENO 0
#endif
#if !defined(STDOUT_FILENO)
#define STDOUT_FILENO 1
#endif
#if !defined(STDERR_FILENO)
#define STDERR_FILENO 2
#endif
namespace hex {
static LONG_PTR s_oldWndProc;
static float s_titleBarHeight;
static Microsoft::WRL::ComPtr<ITaskbarList4> s_taskbarList;
static bool s_useLayeredWindow = true;
void nativeErrorMessage(const std::string &message) {
log::fatal(message);
MessageBoxA(nullptr, message.c_str(), "Error", MB_ICONERROR | MB_OK);
}
// Custom Window procedure for receiving OS events
static LRESULT commonWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_DPICHANGED: {
int interfaceScaleSetting = int(hex::ContentRegistry::Settings::read<float>("hex.builtin.setting.interface", "hex.builtin.setting.interface.scaling_factor", 0.0F) * 10.0F);
if (interfaceScaleSetting != 0)
break;
const auto newScale = LOWORD(wParam) / 96.0F;
const auto oldScale = ImHexApi::System::getNativeScale();
if (u32(oldScale * 10) == u32(newScale * 10))
break;
EventDPIChanged::post(oldScale, newScale);
ImHexApi::System::impl::setNativeScale(newScale);
ThemeManager::reapplyCurrentTheme();
ImGui::GetStyle().ScaleAllSizes(newScale);
return TRUE;
}
case WM_COPYDATA: {
// Handle opening files in existing instance
auto message = reinterpret_cast<COPYDATASTRUCT *>(lParam);
if (message == nullptr)
break;
const auto data = reinterpret_cast<const u8*>(message->lpData);
const auto size = message->cbData;
EventNativeMessageReceived::post(std::vector<u8>(data, data + size));
break;
}
case WM_SETTINGCHANGE: {
// Handle Windows theme changes
if (lParam == 0) break;
if (reinterpret_cast<const WCHAR*>(lParam) == std::wstring_view(L"ImmersiveColorSet")) {
EventOSThemeChanged::post();
}
break;
}
case WM_SETCURSOR: {
if (LOWORD(lParam) != HTCLIENT) {
return CallWindowProc(reinterpret_cast<WNDPROC>(s_oldWndProc), hwnd, uMsg, wParam, lParam);
} else {
switch (ImGui::GetMouseCursor()) {
case ImGuiMouseCursor_Arrow:
SetCursor(LoadCursor(nullptr, IDC_ARROW));
break;
case ImGuiMouseCursor_Hand:
SetCursor(LoadCursor(nullptr, IDC_HAND));
break;
case ImGuiMouseCursor_ResizeEW:
SetCursor(LoadCursor(nullptr, IDC_SIZEWE));
break;
case ImGuiMouseCursor_ResizeNS:
SetCursor(LoadCursor(nullptr, IDC_SIZENS));
break;
case ImGuiMouseCursor_ResizeNWSE:
SetCursor(LoadCursor(nullptr, IDC_SIZENWSE));
break;
case ImGuiMouseCursor_ResizeNESW:
SetCursor(LoadCursor(nullptr, IDC_SIZENESW));
break;
case ImGuiMouseCursor_ResizeAll:
SetCursor(LoadCursor(nullptr, IDC_SIZEALL));
break;
case ImGuiMouseCursor_NotAllowed:
SetCursor(LoadCursor(nullptr, IDC_NO));
break;
case ImGuiMouseCursor_TextInput:
SetCursor(LoadCursor(nullptr, IDC_IBEAM));
break;
default:
break;
}
return TRUE;
}
}
default:
break;
}
return CallWindowProc(reinterpret_cast<WNDPROC>(s_oldWndProc), hwnd, uMsg, wParam, lParam);
}
// Custom window procedure for borderless window
static LRESULT borderlessWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_MOUSELAST:
break;
case WM_NCACTIVATE:
case WM_NCPAINT:
// Handle Windows Aero Snap
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case WM_NCCALCSIZE: {
// Handle window resizing
RECT &rect = *reinterpret_cast<RECT *>(lParam);
RECT client = rect;
CallWindowProc(reinterpret_cast<WNDPROC>(s_oldWndProc), hwnd, uMsg, wParam, lParam);
if (IsMaximized(hwnd)) {
WINDOWINFO windowInfo = { };
windowInfo.cbSize = sizeof(WINDOWINFO);
GetWindowInfo(hwnd, &windowInfo);
rect = RECT {
.left = static_cast<LONG>(client.left + windowInfo.cyWindowBorders),
.top = static_cast<LONG>(client.top + windowInfo.cyWindowBorders),
.right = static_cast<LONG>(client.right - windowInfo.cyWindowBorders),
.bottom = static_cast<LONG>(client.bottom - windowInfo.cyWindowBorders) + 1
};
} else {
rect = client;
}
// This code tries to avoid DWM flickering when resizing the window
// It's not perfect, but it's really the best we can do.
LARGE_INTEGER performanceFrequency = {};
QueryPerformanceFrequency(&performanceFrequency);
TIMECAPS tc = {};
timeGetDevCaps(&tc, sizeof(tc));
const auto granularity = tc.wPeriodMin;
timeBeginPeriod(tc.wPeriodMin);
DWM_TIMING_INFO dti = {};
dti.cbSize = sizeof(dti);
::DwmGetCompositionTimingInfo(nullptr, &dti);
LARGE_INTEGER end = {};
QueryPerformanceCounter(&end);
const auto period = dti.qpcRefreshPeriod;
const i64 delta = dti.qpcVBlank - end.QuadPart;
i64 sleepTicks = 0;
i64 sleepMilliSeconds = 0;
if (period > 0) {
if (delta >= 0) {
sleepTicks = delta / period;
} else {
sleepTicks = -1 + delta / period;
}
sleepMilliSeconds = delta - (period * sleepTicks);
const double sleepTime = std::round(1000.0 * double(sleepMilliSeconds) / double(performanceFrequency.QuadPart));
if (sleepTime >= 0.0) {
Sleep(DWORD(sleepTime));
}
}
timeEndPeriod(granularity);
return WVR_REDRAW;
}
case WM_ERASEBKGND:
return 1;
case WM_WINDOWPOSCHANGING: {
// Make sure that windows discards the entire client area when resizing to avoid flickering
const auto windowPos = reinterpret_cast<LPWINDOWPOS>(lParam);
windowPos->flags |= SWP_NOCOPYBITS;
break;
}
case WM_NCHITTEST: {
// Handle window resizing and moving
POINT cursor = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
const POINT border {
static_cast<LONG>((::GetSystemMetrics(SM_CXFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale()),
static_cast<LONG>((::GetSystemMetrics(SM_CYFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale())
};
if (glfwGetWindowMonitor(ImHexApi::System::getMainWindowHandle()) != nullptr) {
return HTCLIENT;
}
RECT window;
if (!::GetWindowRect(hwnd, &window)) {
return HTNOWHERE;
}
constexpr static auto RegionClient = 0b0000;
constexpr static auto RegionLeft = 0b0001;
constexpr static auto RegionRight = 0b0010;
constexpr static auto RegionTop = 0b0100;
constexpr static auto RegionBottom = 0b1000;
const auto result =
RegionLeft * (cursor.x < (window.left + border.x)) |
RegionRight * (cursor.x >= (window.right - border.x)) |
RegionTop * (cursor.y < (window.top + border.y)) |
RegionBottom * (cursor.y >= (window.bottom - border.y));
if (result != 0 && (ImGui::IsAnyItemHovered())) {
break;
}
if (ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) {
if (result == RegionClient)
return HTCLIENT;
else
return HTCAPTION;
}
std::string_view hoveredWindowName = ImGui::GetCurrentContext()->HoveredWindow == nullptr ? "" : GImGui->HoveredWindow->Name;
if (!ImHexApi::System::impl::isWindowResizable()) {
if (result != RegionClient) {
return HTCAPTION;
}
}
switch (result) {
case RegionLeft:
return HTLEFT;
case RegionRight:
return HTRIGHT;
case RegionTop:
return HTTOP;
case RegionBottom:
return HTBOTTOM;
case RegionTop | RegionLeft:
return HTTOPLEFT;
case RegionTop | RegionRight:
return HTTOPRIGHT;
case RegionBottom | RegionLeft:
return HTBOTTOMLEFT;
case RegionBottom | RegionRight:
return HTBOTTOMRIGHT;
case RegionClient:
default:
if (cursor.y < (window.top + s_titleBarHeight * 2)) {
if (hoveredWindowName == "##MainMenuBar" || hoveredWindowName == "ImHexDockSpace") {
if (!ImGui::IsAnyItemHovered()) {
return HTCAPTION;
}
}
}
break;
}
break;
}
default:
break;
}
return commonWindowProc(hwnd, uMsg, wParam, lParam);
}
[[maybe_unused]]
static void reopenConsoleHandle(u32 stdHandleNumber, i32 stdFileDescriptor, FILE *stdStream) {
// Get the Windows handle for the standard stream
HANDLE handle = ::GetStdHandle(stdHandleNumber);
// Redirect the standard stream to the relevant console stream
if (stdFileDescriptor == STDIN_FILENO)
freopen("CONIN$", "rt", stdStream);
else
freopen("CONOUT$", "wt", stdStream);
// Disable buffering
setvbuf(stdStream, nullptr, _IONBF, 0);
// Reopen the standard stream as a file descriptor
auto unboundFd = [stdFileDescriptor, handle]{
if (stdFileDescriptor == STDIN_FILENO)
return _open_osfhandle(intptr_t(handle), _O_RDONLY);
else
return _open_osfhandle(intptr_t(handle), _O_WRONLY);
}();
// Duplicate the file descriptor to the standard stream
dup2(unboundFd, stdFileDescriptor);
}
void enumerateFonts() {
constexpr static auto FontRegistryPath = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
static const std::array RegistryLocations = {
HKEY_LOCAL_MACHINE,
HKEY_CURRENT_USER
};
for (const auto location : RegistryLocations) {
HKEY key;
if (RegOpenKeyExW(location, FontRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) {
continue;
}
DWORD index = 0;
std::wstring valueName(0xFFF, L'\0');
DWORD valueNameSize = valueName.size() * sizeof(wchar_t);
std::wstring valueData(0xFFF, L'\0');
DWORD valueDataSize = valueData.size() * sizeof(wchar_t);
DWORD valueType;
while (RegEnumValueW(key, index, valueName.data(), &valueNameSize, nullptr, &valueType, reinterpret_cast<BYTE *>(valueData.data()), &valueDataSize) == ERROR_SUCCESS) {
if (valueType == REG_SZ) {
auto fontName = hex::utf16ToUtf8(valueName.c_str());
auto fontPath = std::fs::path(valueData);
if (fontPath.is_relative())
fontPath = std::fs::path("C:\\Windows\\Fonts") / fontPath;
registerFont(fontName.c_str(), wolv::util::toUTF8String(fontPath).c_str());
}
valueNameSize = valueName.size();
valueDataSize = valueData.size();
index++;
}
RegCloseKey(key);
}
}
void Window::configureGLFW() {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_DECORATED, ImHexApi::System::isBorderlessWindowModeEnabled() ? GL_FALSE : GL_TRUE);
// Windows versions before Windows 10 have issues with transparent framebuffers
// causing the entire window to be slightly transparent ignoring all configurations
if (::IsWindows10OrGreater()) {
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
} else {
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_FALSE);
}
}
void Window::initNative() {
if (ImHexApi::System::isDebugBuild()) {
// If the application is running in debug mode, ImHex runs under the CONSOLE subsystem,
// so we don't need to do anything besides enabling ANSI colors
log::impl::enableColorPrinting();
} else if (hex::getEnvironmentVariable("__IMHEX_FORWARD_CONSOLE__") == "1") {
// Check for the __IMHEX_FORWARD_CONSOLE__ environment variable that was set by the forwarder application
// Enable ANSI colors in the console
log::impl::enableColorPrinting();
} else {
log::impl::redirectToFile();
}
// Add plugin library folders to dll search path
for (const auto &path : paths::Libraries.read()) {
if (std::fs::exists(path))
AddDllDirectory(path.c_str());
}
enumerateFonts();
}
class DropManager : public IDropTarget {
public:
DropManager() = default;
virtual ~DropManager() = default;
ULONG STDMETHODCALLTYPE AddRef() override { return 1; }
ULONG STDMETHODCALLTYPE Release() override { return 0; }
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override {
if (riid == IID_IDropTarget) {
*ppvObject = this;
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE DragEnter(
IDataObject *,
DWORD,
POINTL,
DWORD *pdwEffect) override
{
EventFileDragged::post(true);
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
HRESULT STDMETHODCALLTYPE DragOver(
DWORD,
POINTL,
DWORD *pdwEffect) override
{
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
HRESULT STDMETHODCALLTYPE DragLeave() override
{
EventFileDragged::post(false);
return S_OK;
}
HRESULT STDMETHODCALLTYPE Drop(
IDataObject *pDataObj,
DWORD,
POINTL,
DWORD *pdwEffect) override
{
FORMATETC fmte = { CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stgm;
if (SUCCEEDED(pDataObj->GetData(&fmte, &stgm))) {
auto hdrop = HDROP(stgm.hGlobal);
auto fileCount = DragQueryFile(hdrop, 0xFFFFFFFF, nullptr, 0);
for (UINT i = 0; i < fileCount; i++) {
WCHAR szFile[MAX_PATH];
UINT cch = DragQueryFileW(hdrop, i, szFile, MAX_PATH);
if (cch > 0 && cch < MAX_PATH) {
EventFileDropped::post(szFile);
}
}
ReleaseStgMedium(&stgm);
}
EventFileDragged::post(false);
*pdwEffect &= DROPEFFECT_COPY;
return S_OK;
}
};
void Window::setupNativeWindow() {
// Setup borderless window
auto hwnd = glfwGetWin32Window(m_window);
CoInitialize(nullptr);
OleInitialize(nullptr);
static DropManager dm;
if (RegisterDragDrop(hwnd, &dm) != S_OK) {
log::warn("Failed to register drop target");
// Register fallback drop target using glfw
glfwSetDropCallback(m_window, [](GLFWwindow *, int count, const char **paths) {
for (int i = 0; i < count; i++) {
EventFileDropped::post(reinterpret_cast<const char8_t *>(paths[i]));
}
});
EventFileDropped::subscribe([this] {
this->unlockFrameRate();
});
}
bool borderlessWindowMode = ImHexApi::System::isBorderlessWindowModeEnabled();
// Set up the correct window procedure based on the borderless window mode state
if (borderlessWindowMode) {
s_oldWndProc = ::SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(borderlessWindowProc));
MARGINS borderless = { 1, 1, 1, 1 };
::DwmExtendFrameIntoClientArea(hwnd, &borderless);
DWORD attribute = DWMNCRP_ENABLED;
::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &attribute, sizeof(attribute));
::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_NOSIZE | SWP_NOMOVE);
} else {
s_oldWndProc = ::SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(commonWindowProc));
}
// Set up a taskbar progress handler
{
if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) {
CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList4, &s_taskbarList);
}
EventSetTaskBarIconState::subscribe([hwnd](u32 state, u32 type, u32 progress){
using enum ImHexApi::System::TaskProgressState;
switch (ImHexApi::System::TaskProgressState(state)) {
case Reset:
s_taskbarList->SetProgressState(hwnd, TBPF_NOPROGRESS);
s_taskbarList->SetProgressValue(hwnd, 0, 0);
break;
case Flash:
FlashWindow(hwnd, true);
break;
case Progress:
s_taskbarList->SetProgressState(hwnd, TBPF_INDETERMINATE);
s_taskbarList->SetProgressValue(hwnd, progress, 100);
break;
}
using enum ImHexApi::System::TaskProgressType;
switch (ImHexApi::System::TaskProgressType(type)) {
case Normal:
s_taskbarList->SetProgressState(hwnd, TBPF_NORMAL);
break;
case Warning:
s_taskbarList->SetProgressState(hwnd, TBPF_PAUSED);
break;
case Error:
s_taskbarList->SetProgressState(hwnd, TBPF_ERROR);
break;
}
});
}
struct ACCENTPOLICY {
u32 accentState;
u32 accentFlags;
u32 gradientColor;
u32 animationId;
};
struct WINCOMPATTRDATA {
int attribute;
PVOID pData;
ULONG dataSize;
};
EventThemeChanged::subscribe([this]{
auto hwnd = glfwGetWin32Window(m_window);
static auto user32Dll = LoadLibraryA("user32.dll");
if (user32Dll != nullptr) {
using SetWindowCompositionAttributeFunc = BOOL(WINAPI*)(HWND, WINCOMPATTRDATA*);
const auto setWindowCompositionAttribute =
reinterpret_cast<SetWindowCompositionAttributeFunc>(
reinterpret_cast<void*>(
GetProcAddress(user32Dll, "SetWindowCompositionAttribute")
)
);
if (setWindowCompositionAttribute != nullptr) {
ACCENTPOLICY policy = { ImGuiExt::GetCustomStyle().WindowBlur > 0.5F ? 4U : 0U, 0, ImGuiExt::GetCustomColorU32(ImGuiCustomCol_BlurBackground), 0 };
WINCOMPATTRDATA data = { 19, &policy, sizeof(ACCENTPOLICY) };
setWindowCompositionAttribute(hwnd, &data);
}
}
});
RequestChangeTheme::subscribe([this](const std::string &theme) {
auto hwnd = glfwGetWin32Window(m_window);
BOOL value = theme == "Dark" ? TRUE : FALSE;
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
});
ImGui::GetIO().ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent();
glfwSetFramebufferSizeCallback(m_window, [](GLFWwindow* window, int width, int height) {
auto *win = static_cast<Window *>(glfwGetWindowUserPointer(window));
win->unlockFrameRate();
glViewport(0, 0, width, height);
ImHexApi::System::impl::setMainWindowSize(width, height);
});
DwmEnableMMCSS(TRUE);
{
constexpr BOOL value = TRUE;
DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_ENABLED, &value, sizeof(value));
}
{
constexpr DWMNCRENDERINGPOLICY value = DWMNCRP_ENABLED;
DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &value, sizeof(value));
}
glfwSetWindowRefreshCallback(m_window, [](GLFWwindow *window) {
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
win->fullFrame();
DwmFlush();
});
// AMD GPUs seem to have issues with Layered Window rendering. Until we figure out
// why that is or AMD fixes the issue on their side, disable it on these GPUs.
s_useLayeredWindow = ImHexApi::System::getGPUVendor() != "ATI Technologies Inc.";
}
void Window::beginNativeWindowFrame() {
s_titleBarHeight = ImGui::GetCurrentWindowRead()->MenuBarHeight;
// Remove WS_POPUP style from the window to make various window management tools work
auto hwnd = glfwGetWin32Window(m_window);
{
auto style = GetWindowLong(hwnd, GWL_STYLE);
style |= WS_OVERLAPPEDWINDOW;
style &= ~WS_POPUP;
::SetWindowLong(hwnd, GWL_STYLE, style);
}
// Make window composited and layered when supported to eradicate any window flickering that happens while resizing
{
auto style = GetWindowLong(hwnd, GWL_EXSTYLE);
style |= WS_EX_COMPOSITED;
if (s_useLayeredWindow)
style |= WS_EX_LAYERED;
::SetWindowLong(hwnd, GWL_EXSTYLE, style);
}
if (!ImHexApi::System::impl::isWindowResizable()) {
if (glfwGetWindowAttrib(m_window, GLFW_MAXIMIZED) == GLFW_TRUE) {
glfwRestoreWindow(m_window);
}
}
}
void Window::endNativeWindowFrame() {
}
}
#endif