// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "app.h" #include "config/settings.h" #include "config/version.h" #include "rpc/rpc_worker.h" #include "ui/schema/ui_schema.h" #include "ui/effects/low_spec.h" #include "ui/notifications.h" #include "ui/theme.h" #include "ui/material/color_theme.h" #include "ui/material/typography.h" #include "ui/material/draw_helpers.h" #include "ui/layout.h" #include "util/single_instance.h" #include "util/payment_uri.h" #include "util/perf_log.h" #include "imgui.h" #include "imgui_internal.h" #include "imgui_impl_sdl3.h" #include "ui/effects/imgui_acrylic.h" #include "ui/effects/theme_effects.h" #include "util/texture_loader.h" #include "util/platform.h" #include "resources/embedded_resources.h" // Backend-specific headers #ifdef DRAGONX_USE_DX11 #include "imgui_impl_dx11.h" #include "platform/dx11_context.h" #else #include "imgui_impl_opengl3.h" #include #ifdef DRAGONX_HAS_GLAD #include #endif #endif #include #include #ifdef _WIN32 #include "platform/windows_backdrop.h" #include #include #include #include #include // SetCurrentProcessExplicitAppUserModelID lives behind NTDDI_WIN7 in // MinGW's shobjidl.h. Rather than forcing the version macro globally, // declare just the one function we need (available on Windows 7+). extern "C" HRESULT __stdcall SetCurrentProcessExplicitAppUserModelID(PCWSTR AppID); // SHGetPropertyStoreForWindow is also behind NTDDI_WIN7 in MinGW headers. extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, void** ppv); // Not defined in older MinGW SDK headers #ifndef WS_EX_NOREDIRECTIONBITMAP #define WS_EX_NOREDIRECTIONBITMAP 0x00200000L #endif // Needed by the borderless WndProc for WM_NCHITTEST #ifndef GET_X_LPARAM #define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp)) #endif #ifndef GET_Y_LPARAM #define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) #endif #endif #include #include #include #include #include #include #include "util/logger.h" #ifdef _WIN32 // Windows crash handler — writes a minidump and log entry before exit static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep) { // Write crash info to the debug log and a dedicated crash log const char* crashMsg = "\n=== CRASH DETECTED ==="; DEBUG_LOGF("%s", crashMsg); DEBUG_LOGF("\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode); DEBUG_LOGF("\nException address: %p", ep->ExceptionRecord->ExceptionAddress); if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) { DEBUG_LOGF("\nAccess type: %s, target address: %p", ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ", (void*)ep->ExceptionRecord->ExceptionInformation[1]); } DEBUG_LOGF("\n=== END CRASH INFO ===\n"); fflush(stdout); fflush(stderr); // Also write to a separate crash log file try { std::string crashPath = (std::filesystem::path( dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string(); FILE* f = fopen(crashPath.c_str(), "a"); if (f) { fprintf(f, "\n=== CRASH at PID %lu ===", (unsigned long)GetCurrentProcessId()); fprintf(f, "\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode); fprintf(f, "\nException address: %p", ep->ExceptionRecord->ExceptionAddress); if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) { fprintf(f, "\nAccess type: %s, target address: %p", ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ", (void*)ep->ExceptionRecord->ExceptionInformation[1]); } fprintf(f, "\n=== END CRASH INFO ===\n"); fclose(f); } } catch (...) {} return EXCEPTION_EXECUTE_HANDLER; } // Set the window's shell property store so Task Manager, taskbar, and shell // always show "ObsidianDragon" regardless of any cached VERSIONINFO metadata. static void SetWindowIdentity(HWND hwnd) { IPropertyStore* pps = nullptr; HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void**)&pps); if (SUCCEEDED(hr) && pps) { // Set AppUserModel.ID on the window (overrides process-level ID for this window) PROPVARIANT pvId; PropVariantInit(&pvId); pvId.vt = VT_LPWSTR; pvId.pwszVal = const_cast(L"DragonX.ObsidianDragon.Wallet"); pps->SetValue(PKEY_AppUserModel_ID, pvId); // Don't PropVariantClear — the string is a static literal // Set RelaunchDisplayNameResource so the shell shows our name PROPVARIANT pvName; PropVariantInit(&pvName); pvName.vt = VT_LPWSTR; pvName.pwszVal = const_cast(L"ObsidianDragon"); pps->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, pvName); // Set RelaunchCommand (required alongside RelaunchDisplayNameResource) PROPVARIANT pvCmd; PropVariantInit(&pvCmd); pvCmd.vt = VT_LPWSTR; wchar_t exePath[MAX_PATH] = {}; GetModuleFileNameW(nullptr, exePath, MAX_PATH); pvCmd.pwszVal = exePath; pps->SetValue(PKEY_AppUserModel_RelaunchCommand, pvCmd); pps->Commit(); pps->Release(); DEBUG_LOGF("Window property store: identity set to ObsidianDragon\n"); } else { DEBUG_LOGF("SHGetPropertyStoreForWindow failed: 0x%08lx\n", (unsigned long)hr); } } #endif // --------------------------------------------------------------- // Borderless window support (DX11 / Windows only) // --------------------------------------------------------------- #ifdef DRAGONX_USE_DX11 static WNDPROC g_sdlWndProc = nullptr; // Caption-zone state: updated every frame by renderBorderlessControls() // so that WM_NCHITTEST can return HTCAPTION for window dragging. static float g_captionHeight = 20.0f; // height of the title/menu bar (px) static float g_captionBtnStartX = 0.0f; // left edge of the min/max/close buttons static bool g_captionImGuiHover = false; // true when ImGui has a hovered/active item static float g_borderlessDpi = 1.0f; // DPI scale, updated from SDL each frame // Custom WndProc that removes the native title bar by extending the // client area to cover the entire window (WM_NCCALCSIZE) and provides // resize borders + caption drag zone (WM_NCHITTEST). static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_NCCALCSIZE: if (wParam == TRUE) { // Returning 0 makes the entire window rect into client area, // which effectively removes the title bar. When maximized, // constrain to the monitor work area so content isn't clipped // behind the taskbar. WINDOWPLACEMENT wp = {}; wp.length = sizeof(wp); GetWindowPlacement(hwnd, &wp); if (wp.showCmd == SW_MAXIMIZE) { auto* p = reinterpret_cast(lParam); HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); MONITORINFO mi = {}; mi.cbSize = sizeof(mi); GetMonitorInfo(mon, &mi); p->rgrc[0] = mi.rcWork; } return 0; } break; case WM_NCHITTEST: { POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; ScreenToClient(hwnd, &pt); RECT rc; GetClientRect(hwnd, &rc); // Resize borders (not when maximized). WINDOWPLACEMENT wp = {}; wp.length = sizeof(wp); GetWindowPlacement(hwnd, &wp); if (wp.showCmd != SW_MAXIMIZE) { const int bdr = (int)(5.0f * g_borderlessDpi); bool t = pt.y <= bdr, b = pt.y >= rc.bottom - bdr; bool l = pt.x <= bdr, r = pt.x >= rc.right - bdr; if (t && l) return HTTOPLEFT; if (t && r) return HTTOPRIGHT; if (b && l) return HTBOTTOMLEFT; if (b && r) return HTBOTTOMRIGHT; if (t) return HTTOP; if (b) return HTBOTTOM; if (l) return HTLEFT; if (r) return HTRIGHT; } // Caption zone: the top bar where the menu + window buttons live. // Return HTCAPTION so Windows handles drag/move natively — but // only when the cursor is NOT over the window-control buttons and // NOT over an ImGui interactive item (e.g. a menu-bar entry). if (pt.y < (int)g_captionHeight) { // Window-control buttons (min/max/close) on the right if (pt.x >= (int)g_captionBtnStartX) return HTCLIENT; // ImGui menu item hovered — let ImGui handle the click if (g_captionImGuiHover) return HTCLIENT; return HTCAPTION; } return HTCLIENT; } case WM_SETCURSOR: // Prevent DefWindowProc from resetting the cursor to the window- // class cursor (IDC_ARROW) on every mouse move. Our render loop // calls SDL_SetCursor() each frame based on ImGui's cursor state; // without this, WM_SETCURSOR fires between frames and reverts the // hand/resize cursor back to the arrow. if (LOWORD(lParam) == HTCLIENT) return TRUE; // "handled" — keep whatever cursor we last set break; case WM_GETMINMAXINFO: { auto* mmi = reinterpret_cast(lParam); // Scale min window size by DPI so it matches the SDL-side minimum. mmi->ptMinTrackSize.x = (LONG)(1024 * g_borderlessDpi); mmi->ptMinTrackSize.y = (LONG)(720 * g_borderlessDpi); return 0; } } return CallWindowProcW(g_sdlWndProc, hwnd, msg, wParam, lParam); } // Draw custom window-control buttons (minimize / maximize-restore / close) // on the foreground draw list, and handle title-bar dragging. static void renderBorderlessControls(SDL_Window* window, HWND /*hwnd*/, dragonx::App& app) { ImGuiIO& io = ImGui::GetIO(); // Use the main viewport's foreground draw list so buttons always render // on the primary window (not on a secondary viewport that has focus). ImGuiViewport* vp = ImGui::GetMainViewport(); ImDrawList* fg = ImGui::GetForegroundDrawList(vp); float captionH = ImGui::GetFrameHeight(); // same height as menu bar float displayW = io.DisplaySize.x; // DPI scale factor for borderless controls float dp = dragonx::ui::Layout::dpiScale(); // With multi-viewport, draw-list coordinates and io.MousePos are in // OS screen-space. Offset all positions by the main viewport origin. const float ox = vp->Pos.x; const float oy = vp->Pos.y; const float btnW = 46.0f * dp; const float btnH = captionH; float closeX = ox + displayW - btnW; float maxX = closeX - btnW; float minX = maxX - btnW; ImVec2 mp = io.MousePos; bool click = ImGui::IsMouseClicked(ImGuiMouseButton_Left); const auto& themeS = dragonx::ui::schema::UI(); ImU32 wcHover = themeS.resolveColor("var(--window-control-hover)", IM_COL32(255, 255, 255, 30)); ImU32 wcIcon = themeS.resolveColor("var(--window-control)", IM_COL32(255, 255, 255, 200)); ImU32 wcClose = themeS.resolveColor("var(--window-close-hover)", IM_COL32(232, 17, 35, 200)); // Icon glyph half-size and line thickness, scaled by DPI const float iconHalf = 5.0f * dp; const float lineThk = 1.0f * dp; // --- Minimize button --- { ImVec2 p0(minX, oy), p1(minX + btnW, oy + btnH); bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y); if (hov) fg->AddRectFilled(p0, p1, wcHover); float cy = oy + btnH * 0.5f, lx = minX + btnW * 0.5f - iconHalf; fg->AddLine(ImVec2(lx, cy), ImVec2(lx + iconHalf * 2.0f, cy), wcIcon, lineThk); if (hov && click) SDL_MinimizeWindow(window); } // --- Maximize / Restore button --- { ImVec2 p0(maxX, oy), p1(maxX + btnW, oy + btnH); bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y); if (hov) fg->AddRectFilled(p0, p1, wcHover); bool maximized = (SDL_GetWindowFlags(window) & SDL_WINDOW_MAXIMIZED) != 0; float cx = maxX + btnW * 0.5f, cy = oy + btnH * 0.5f; if (maximized) { float s1 = 4.0f * dp, s2 = 3.0f * dp; float s3 = 2.0f * dp, s4 = 5.0f * dp; fg->AddRect(ImVec2(cx - s1, cy - s2), ImVec2(cx + s2, cy + s1), wcIcon, 0, 0, lineThk); fg->AddRect(ImVec2(cx - s3, cy - s4), ImVec2(cx + s4, cy + s3), wcIcon, 0, 0, lineThk); } else { fg->AddRect(ImVec2(cx - iconHalf, cy - iconHalf + dp), ImVec2(cx + iconHalf, cy + iconHalf - dp), wcIcon, 0, 0, lineThk); } if (hov && click) { if (maximized) SDL_RestoreWindow(window); else SDL_MaximizeWindow(window); } } // --- Close button --- { ImVec2 p0(closeX, oy), p1(closeX + btnW, oy + btnH); bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y); if (hov) fg->AddRectFilled(p0, p1, wcClose); float cx = closeX + btnW * 0.5f, cy = oy + btnH * 0.5f; fg->AddLine(ImVec2(cx - iconHalf, cy - iconHalf), ImVec2(cx + iconHalf, cy + iconHalf), wcIcon, lineThk); fg->AddLine(ImVec2(cx + iconHalf, cy - iconHalf), ImVec2(cx - iconHalf, cy + iconHalf), wcIcon, lineThk); if (hov && click) app.beginShutdown(); } // --- Update caption-zone state for WM_NCHITTEST --- // WM_NCHITTEST uses client-local coordinates, so subtract viewport origin. g_captionHeight = captionH; g_captionBtnStartX = minX - ox; g_captionImGuiHover = ImGui::IsAnyItemHovered() || ImGui::IsAnyItemActive(); g_borderlessDpi = dp; } #endif // DRAGONX_USE_DX11 // Forward declarations static bool InitSDL(); #ifdef DRAGONX_USE_DX11 static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx); static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx); #else static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context); static void Shutdown(SDL_Window* window, SDL_GLContext gl_context); #endif // Global single instance lock static dragonx::util::SingleInstance g_single_instance("obsidiandragon"); // Check for payment URI in command line args static std::string findPaymentURI(int argc, char* argv[]) { for (int i = 1; i < argc; i++) { if (dragonx::util::isPaymentURI(argv[i])) { return argv[i]; } } return ""; } int main(int argc, char* argv[]) { // Ensure ObsidianDragon config directory exists early (before any file I/O) { std::string odDir = dragonx::util::Platform::getObsidianDragonDir(); std::error_code ec; std::filesystem::create_directories(odDir, ec); } #ifdef _WIN32 // Redirect stdout/stderr to a log file so diagnostic output is visible // even when built as a GUI app (WIN32_EXECUTABLE hides the console). { std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string(); freopen(logPath.c_str(), "w", stdout); freopen(logPath.c_str(), "a", stderr); } // Install crash handler for diagnostics SetUnhandledExceptionFilter(CrashHandler); // Set the Application User Model ID so Windows Task Manager, taskbar, // and jump lists show "ObsidianDragon" instead of inheriting a // description from the MinGW runtime ("POSIX WinThreads for Windows"). SetCurrentProcessExplicitAppUserModelID(L"DragonX.ObsidianDragon.Wallet"); #endif // Check for payment URI in command line std::string pendingURI = findPaymentURI(argc, argv); DEBUG_LOGF("%s v%s\n", DRAGONX_APP_NAME, DRAGONX_VERSION); DEBUG_LOGF("Built with Dear ImGui %s\n", IMGUI_VERSION); fflush(stdout); // Check for existing instance if (!g_single_instance.tryLock()) { fprintf(stderr, "Another instance of ObsidianDragon is already running.\n"); DEBUG_LOGF("Please close the existing instance first.\n"); #ifdef _WIN32 MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.", L"ObsidianDragon", MB_OK | MB_ICONINFORMATION); #endif return 1; } // Initialize SDL if (!InitSDL()) { fprintf(stderr, "Failed to initialize SDL!\n"); #ifdef _WIN32 MessageBoxW(nullptr, L"Failed to initialize SDL. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR); #endif return 1; } // Load saved window size early (before window creation, before App exists). // Settings are loaded again later in app.init(), this is just for the size. int savedWinW = 1200, savedWinH = 720; { dragonx::config::Settings earlySettings; if (earlySettings.load()) { int sw = earlySettings.getWindowWidth(); int sh = earlySettings.getWindowHeight(); if (sw >= 1024 && sh >= 720) { savedWinW = sw; savedWinH = sh; DEBUG_LOGF("Restored window size from settings: %dx%d\n", sw, sh); } } } // --------------------------------------------------------------- // Platform-specific window creation and graphics context // --------------------------------------------------------------- #ifdef DRAGONX_USE_DX11 // DirectX 11 path (Windows) // // We create the native Win32 HWND ourselves so we can set // WS_EX_NOREDIRECTIONBITMAP at creation time. This style MUST be // present when the window is first created -- setting it later with // SetWindowLongPtrW does NOT reliably release the opaque redirection // surface that DWM allocates. Without it, the legacy redirection // surface paints over our transparent DirectComposition visual. // This is the same approach Windows Terminal uses. // Register a minimal window class WNDCLASSEXW wc = {}; wc.cbSize = sizeof(WNDCLASSEXW); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = DefWindowProcW; wc.hInstance = GetModuleHandleW(nullptr); wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW // Load application icon from .exe resources (.rc embeds IDI_ICON1 = ordinal 1) wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(1)); wc.hIconSm = (HICON)LoadImageW(wc.hInstance, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR); wc.lpszClassName = L"ObsidianDragon"; RegisterClassExW(&wc); // Compute centered position including frame // Use saved window size from settings if available, otherwise default int winW = savedWinW, winH = savedWinH; DWORD dwStyle = WS_OVERLAPPEDWINDOW; DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP; RECT wr = {0, 0, winW, winH}; AdjustWindowRectEx(&wr, dwStyle, FALSE, dwExStyle); int adjW = wr.right - wr.left; int adjH = wr.bottom - wr.top; int posX = (GetSystemMetrics(SM_CXSCREEN) - adjW) / 2; int posY = (GetSystemMetrics(SM_CYSCREEN) - adjH) / 2; HWND nativeHwnd = CreateWindowExW( dwExStyle, L"ObsidianDragon", L"ObsidianDragon", dwStyle, posX, posY, adjW, adjH, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); if (!nativeHwnd) { fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError()); MessageBoxW(nullptr, L"Failed to create window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR); SDL_Quit(); return 1; } DEBUG_LOGF("Native HWND created with WS_EX_NOREDIRECTIONBITMAP\n"); // Don't show yet — wait until borderless subclass is installed // so the window appears without a visible native title bar. // Explicitly set the window icon on the HWND for taskbar + Alt-Tab. // The WNDCLASSEXW already sets hIcon/hIconSm, but WM_SETICON ensures // the shell picks up the icon even for borderless windows. { HINSTANCE hInst = GetModuleHandleW(nullptr); HICON bigIcon = LoadIconW(hInst, MAKEINTRESOURCEW(1)); HICON smIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR); if (bigIcon) SendMessageW(nativeHwnd, WM_SETICON, ICON_BIG, (LPARAM)bigIcon); if (smIcon) SendMessageW(nativeHwnd, WM_SETICON, ICON_SMALL, (LPARAM)smIcon); DEBUG_LOGF("Taskbar icon set from .exe resource (big=%p small=%p)\n", (void*)bigIcon, (void*)smIcon); } // Wrap the native HWND with SDL for event handling + ImGui integration. // SDL_WINDOW_TRANSPARENT triggers DwmEnableBlurBehindWindow internally, // which is another signal to DWM to respect per-pixel alpha. SDL_PropertiesID createProps = SDL_CreateProperties(); SDL_SetPointerProperty(createProps, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, (void*)nativeHwnd); SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true); SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true); SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true); SDL_SetStringProperty(createProps, SDL_PROP_WINDOW_CREATE_TITLE_STRING, DRAGONX_APP_NAME); SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, winW); SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, winH); SDL_Window* window = SDL_CreateWindowWithProperties(createProps); SDL_DestroyProperties(createProps); if (window == nullptr) { fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError()); MessageBoxW(nullptr, L"Failed to create SDL window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR); DestroyWindow(nativeHwnd); SDL_Quit(); return 1; } DEBUG_LOGF("SDL window wrapping native HWND (external)\n"); // Subclass for borderless window (removes native title bar). // SDL has already installed its own WndProc; we save it and chain to it. g_sdlWndProc = (WNDPROC)GetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC); SetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC, (LONG_PTR)BorderlessWndProc); // Resize to intended client dimensions (no frame/title-bar overhead) // and trigger a frame recalculation so WM_NCCALCSIZE fires. SetWindowPos(nativeHwnd, nullptr, 0, 0, winW, winH, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOZORDER); ShowWindow(nativeHwnd, SW_SHOW); UpdateWindow(nativeHwnd); DEBUG_LOGF("Borderless window: native title bar removed\n"); // Set shell property store on the HWND so Task Manager and the taskbar // always show "ObsidianDragon" (overrides any cached metadata). SetWindowIdentity(nativeHwnd); // Initialize DirectX 11 context with DXGI alpha swap chain dragonx::platform::DX11Context dx; if (!dx.init(window)) { fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n"); MessageBoxW(nullptr, L"Failed to initialize DirectX 11.\nPlease ensure your graphics drivers are up to date.\n\nCheck the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Graphics Error", MB_OK | MB_ICONERROR); SDL_DestroyWindow(window); SDL_Quit(); return 1; } DEBUG_LOGF("DirectX 11 rendering backend initialized\n"); fflush(stdout); bool backdrop_active = false; #ifdef _WIN32 // Transparency pipeline: // 1) WS_EX_NOREDIRECTIONBITMAP (set at HWND creation) // 2) DirectComposition swap chain with DXGI_ALPHA_MODE_PREMULTIPLIED // 3) DwmEnableBlurBehindWindow with full-window region for blur // 4) On Win11 22H2+: Acrylic (DWMSBT_TRANSIENTWINDOW) for modern blur // // NOTE: Mica/MicaAlt (DWMSBT_MAINWINDOW / DWMSBT_TABBEDWINDOW) are // opaque materials and must NOT be used — they fill the entire window // with a wallpaper-derived color. Acrylic is the correct choice: it // provides gaussian blur of the desktop behind the window. if (dx.hasAlphaCompositing()) { backdrop_active = true; dragonx::ui::material::SetBackdropActive(true); HWND hwnd_blur = (HWND)SDL_GetPointerProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); if (hwnd_blur) { // Override SDL's zero-area blur region with a full-window blur. // This gives DWM the signal to blur everything behind our window. HRGN blurRgn = CreateRectRgn(0, 0, -1, -1); // full window DWM_BLURBEHIND bb = {}; bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; bb.fEnable = TRUE; bb.hRgnBlur = blurRgn; HRESULT hr_blur = DwmEnableBlurBehindWindow(hwnd_blur, &bb); DeleteObject(blurRgn); DEBUG_LOGF("DwmEnableBlurBehindWindow (full region): 0x%08lx\n", hr_blur); // On Windows 11 22H2+, also enable Acrylic for nicer modern blur auto ver = dragonx::platform::getWindowsVersion(); if (ver.build >= 22621) { // Signal to DWM that we handle our own frame rendering. // Use {0,0,1,0} (1 pixel top) instead of {-1,-1,-1,-1} // because full-window margins cause DWM to render its own // caption buttons (min/max/close) on top of ours. // WM_NCCALCSIZE returning 0 already extends the client area // to the full window, which satisfies the system backdrop // requirement. The 1-pixel top margin is enough to keep // DWM's shadow and rounded corners on Win11. MARGINS margins = {0, 0, 1, 0}; DwmExtendFrameIntoClientArea(hwnd_blur, &margins); // Acrylic = blurred, semi-transparent desktop view // (NOT Mica which is opaque) DWORD backdrop = 3; // DWMSBT_TRANSIENTWINDOW = Acrylic HRESULT hr_acr = DwmSetWindowAttribute(hwnd_blur, 38, &backdrop, sizeof(backdrop)); DEBUG_LOGF("DWM Acrylic (DWMSBT_TRANSIENTWINDOW): 0x%08lx\n", hr_acr); BOOL useDarkMode = TRUE; DwmSetWindowAttribute(hwnd_blur, 20, &useDarkMode, sizeof(useDarkMode)); } } DEBUG_LOGF("DX11: Alpha compositing active with blur\n"); fflush(stdout); } else { DEBUG_LOGF("DX11: No alpha compositing - window is opaque\n"); } #endif // Initialize ImGui with DX11 backend if (!InitImGui(window, dx)) { fprintf(stderr, "Failed to initialize ImGui!\n"); MessageBoxW(nullptr, L"Failed to initialize ImGui. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR); Shutdown(window, dx); return 1; } // Initialize acrylic effects system (DX11 backend) if (dragonx::ui::effects::ImGuiAcrylic::Init()) { DEBUG_LOGF("Acrylic effects initialized (DX11)\n"); } else { DEBUG_LOGF("Acrylic effects not available on DX11 (will use fallback colors)\n"); } #else // OpenGL path (Linux / macOS) #ifdef __APPLE__ // macOS requires GL 3.2 Core Profile + GLSL 150 SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); #else // GL 3.0 + GLSL 130 SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #endif // Create window with graphics context SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_TRANSPARENT); SDL_Window* window = SDL_CreateWindow( DRAGONX_APP_NAME, savedWinW, savedWinH, window_flags ); if (window == nullptr) { // Retry without transparency if compositor doesn't support it window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); window = SDL_CreateWindow( DRAGONX_APP_NAME, savedWinW, savedWinH, window_flags ); } if (window == nullptr) { fprintf(stderr, "Error: SDL_CreateWindow(): %s\n", SDL_GetError()); return 1; } SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); SDL_SetWindowMinimumSize(window, 1024, 720); SDL_GLContext gl_context = SDL_GL_CreateContext(window); if (gl_context == nullptr) { fprintf(stderr, "Error: SDL_GL_CreateContext(): %s\n", SDL_GetError()); return 1; } SDL_GL_MakeCurrent(window, gl_context); SDL_GL_SetSwapInterval(1); // Enable vsync bool backdrop_active = false; // Check if the compositor granted window transparency if (SDL_GetWindowFlags(window) & SDL_WINDOW_TRANSPARENT) { backdrop_active = true; dragonx::ui::material::SetBackdropActive(true); DEBUG_LOGF("Window transparency enabled by compositor\n"); } #ifdef DRAGONX_HAS_GLAD // Initialize GLAD - load OpenGL function pointers int glad_version = gladLoadGL((GLADloadfunc)SDL_GL_GetProcAddress); if (!glad_version) { fprintf(stderr, "Error: Failed to initialize GLAD (OpenGL loader)\n"); return 1; } DEBUG_LOGF("GLAD initialized - OpenGL %d.%d loaded\n", GLAD_VERSION_MAJOR(glad_version), GLAD_VERSION_MINOR(glad_version)); // Initialize acrylic effects system if (dragonx::ui::effects::ImGuiAcrylic::Init()) { DEBUG_LOGF("Acrylic effects initialized\n"); // Enable backdrop so glass panels use acrylic blur if (!backdrop_active) { backdrop_active = true; dragonx::ui::material::SetBackdropActive(true); DEBUG_LOGF("Backdrop activated for acrylic effects\n"); } } else { DEBUG_LOGF("Acrylic effects not available (will use fallback colors)\n"); } #endif // Initialize ImGui with OpenGL backend if (!InitImGui(window, gl_context)) { fprintf(stderr, "Failed to initialize ImGui!\n"); Shutdown(window, gl_context); return 1; } #endif // DRAGONX_USE_DX11 // Set window icon from logo_ObsidianDragon.png { std::string iconPath = dragonx::util::getExecutableDirectory() + "/res/img/logos/logo_ObsidianDragon.png"; int iconW = 0, iconH = 0; unsigned char* iconPixels = dragonx::util::LoadRawPixelsFromFile(iconPath.c_str(), &iconW, &iconH); if (!iconPixels) { // Try embedded resource fallback (Windows single-file distribution) const auto* iconRes = dragonx::resources::getEmbeddedResource("logo_ObsidianDragon.png"); if (iconRes && iconRes->data && iconRes->size > 0) { iconPixels = dragonx::util::LoadRawPixelsFromMemory(iconRes->data, iconRes->size, &iconW, &iconH); } } if (iconPixels) { SDL_Surface* iconSurface = SDL_CreateSurfaceFrom(iconW, iconH, SDL_PIXELFORMAT_RGBA32, iconPixels, iconW * 4); if (iconSurface) { SDL_SetWindowIcon(window, iconSurface); SDL_DestroySurface(iconSurface); DEBUG_LOGF("Window icon set (%dx%d)\n", iconW, iconH); } dragonx::util::FreeRawPixels(iconPixels); } else { DEBUG_LOGF("Warning: Could not load window icon from %s or embedded\n", iconPath.c_str()); } } // Apply DragonX Material Design theme dragonx::ui::SetDragonXMaterialTheme(); // Scale ImGui style sizes (padding, rounding, scrollbar, etc.) by the // display DPI factor. On Windows Per-Monitor DPI v2, SDL3 works in // physical pixels and DisplayFramebufferScale is 1.0, so we must // manually scale both fonts (done in Typography::load) and style. // The window is also resized by dpiScale below so everything fits. float currentDpiScale = dragonx::ui::material::Typography::instance().getDpiScale(); #ifdef DRAGONX_USE_DX11 g_borderlessDpi = currentDpiScale; #endif if (currentDpiScale > 1.01f) { ImGui::GetStyle().ScaleAllSizes(currentDpiScale); DEBUG_LOGF("Scaled ImGui style sizes by %.2f for DPI awareness\n", currentDpiScale); } // If we're starting on a HiDPI display, scale the window so the UI // appears the same physical size as on a 100% display. // On macOS with HIGH_PIXEL_DENSITY, window sizes are already in logical // (point) coordinates and the framebuffer handles pixel density, so // we must NOT multiply the window size by the content scale. #ifndef __APPLE__ if (currentDpiScale > 1.01f) { int curW = 0, curH = 0; SDL_GetWindowSize(window, &curW, &curH); int newW = (int)(curW * currentDpiScale); int newH = (int)(curH * currentDpiScale); // Clamp to the display's work area so the window fits on screen. SDL_DisplayID did = SDL_GetDisplayForWindow(window); if (did) { SDL_Rect usable; if (SDL_GetDisplayUsableBounds(did, &usable)) { newW = std::min(newW, usable.w); newH = std::min(newH, usable.h); } } SDL_SetWindowSize(window, newW, newH); SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale)); SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); DEBUG_LOGF("HiDPI startup: window resized %dx%d -> %dx%d (scale %.2f)\n", curW, curH, newW, newH, currentDpiScale); } #endif // Create application instance dragonx::App app; if (!app.init()) { fprintf(stderr, "Failed to initialize application!\n"); #ifdef _WIN32 MessageBoxW(nullptr, L"Failed to initialize application. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log", L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR); #endif #ifdef DRAGONX_USE_DX11 Shutdown(window, dx); #else Shutdown(window, gl_context); #endif return 1; } // Activate backdrop transparency if the skin system loaded a background // image during app.init() → SkinManager::setActiveSkin(). if (app.getGradientTexture() != 0 && !backdrop_active) { backdrop_active = true; dragonx::ui::material::SetBackdropActive(true); // Re-apply TOML-based colors with the new backdrop alpha values. // Must NOT call SetDragonXMaterialTheme() here — that would clobber // the active TOML palette with hardcoded C++ DragonX colors, causing // a visual shift when the user later cycles themes via Ctrl+Arrow. dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui(); } // Initialize performance profiler — writes to /perf.log { std::string perfPath = (std::filesystem::path( dragonx::util::Platform::getObsidianDragonDir()) / "perf.log").string(); dragonx::util::PerfLog::instance().init(perfPath); } // NOTE: No font-scale startup resize here. The saved window size // already includes any font-scale inflation from the previous session // (save divides by dpiScale but not fontScale). Multiplying by // fontScale again would compound the effect on every restart. // The per-frame font-scale block in the main loop handles resizing // when the user actually changes the slider. // Handle pending payment URI from command line if (!pendingURI.empty()) { DEBUG_LOGF("Processing payment URI: %s\n", pendingURI.c_str()); app.handlePaymentURI(pendingURI); } // ================================================================ // Live-resize support (Linux/X11) // On X11, the window manager runs a modal loop during drag-resize, // blocking SDL_PollEvent. We install an event watcher that fires // immediately from the WM callback and performs a full render cycle // so the UI redraws in real time while the window is being resized. // ================================================================ struct ResizeCtx { SDL_Window* window; dragonx::App* app; bool backdrop_active; #ifdef DRAGONX_USE_DX11 dragonx::platform::DX11Context* dx; #else SDL_GLContext gl_context; #endif }; ResizeCtx resizeCtx; resizeCtx.window = window; resizeCtx.app = &app; resizeCtx.backdrop_active = backdrop_active; #ifdef DRAGONX_USE_DX11 resizeCtx.dx = &dx; #else resizeCtx.gl_context = gl_context; #endif auto resizeWatcher = [](void* userdata, SDL_Event* event) -> bool { if (event->type != SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED && event->type != SDL_EVENT_WINDOW_RESIZED) return true; auto* ctx = static_cast(userdata); if (event->window.windowID != SDL_GetWindowID(ctx->window)) return true; // Avoid re-entrancy static bool in_resize_render = false; if (in_resize_render) return true; in_resize_render = true; // Pre-frame: font atlas rebuilds before NewFrame() ctx->app->preFrame(); #ifdef DRAGONX_USE_DX11 ctx->dx->resize(event->window.data1, event->window.data2); ImGui_ImplDX11_NewFrame(); #else SDL_GL_MakeCurrent(ctx->window, ctx->gl_context); ImGui_ImplOpenGL3_NewFrame(); #endif ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); // Background gradient { ImGuiViewport* vp = ImGui::GetMainViewport(); ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); ImVec2 p0 = vp->Pos; ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y); ImTextureID curGradTex2 = ctx->app->getGradientTexture(); float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f; // Scale only background layer alpha by window opacity so the // desktop shows through while UI elements stay fully opaque. auto scaleA2 = [&](ImU32 col) -> ImU32 { uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF; a = (uint8_t)(a * winOp2); return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT); }; if (curGradTex2 != 0) { const auto& S1 = dragonx::ui::schema::UI(); ImU32 baseTop1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200)); ImU32 baseBottom1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200)); int tintAlpha1 = (int)S1.drawElement("backdrop", "texture-tint-alpha").size; if (tintAlpha1 <= 0) tintAlpha1 = 140; bgDL->AddRectFilledMultiColor(p0, p1, scaleA2(baseTop1), scaleA2(baseTop1), scaleA2(baseBottom1), scaleA2(baseBottom1)); int scaledTA1 = (int)(tintAlpha1 * winOp2); ImU32 tintCol1 = IM_COL32(255, 255, 255, scaledTA1); bgDL->AddImage(curGradTex2, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol1); } else if (ctx->backdrop_active) { const auto& S = dragonx::ui::schema::UI(); auto bde = [&](const char* key, float fb) { float v = S.drawElement("backdrop", key).size; return v >= 0 ? v : fb; }; ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12), (int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOp2)); ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8), (int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOp2)); bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom); } // Acrylic capture callback — must fire BEFORE the noise // overlay so the noise grain is not captured and blurred // into the glass cards. auto captureCb2 = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback(); if (captureCb2) { bgDL->AddCallback(captureCb2, nullptr); bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr); } // WindowBg alpha stays at its base theme value — NOT scaled // by window opacity. Only the background gradient/texture // layers are scaled so the desktop shows through while UI // elements (cards, text, buttons) remain fully readable. if (ctx->backdrop_active || curGradTex2 != 0) { const auto& S4 = dragonx::ui::schema::UI(); float baseBgAlpha2 = S4.drawElement("backdrop", "background-alpha").sizeOr(0.40f); ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha2; } } ImGuiIO& io = ImGui::GetIO(); dragonx::ui::effects::ImGuiAcrylic::BeginFrame( (int)io.DisplaySize.x, (int)io.DisplaySize.y); ctx->app->update(); ctx->app->render(); ImGui::Render(); #ifdef DRAGONX_USE_DX11 ctx->dx->ensureSize(); ID3D11RenderTargetView* rtv = ctx->dx->renderTargetView(); ctx->dx->deviceContext()->OMSetRenderTargets(1, &rtv, nullptr); { float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f; bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f; bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq; ctx->dx->clear(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f); } ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); ctx->dx->present(0); // immediate present during resize #else glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); { float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f; bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f; bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq; glClearColor(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f); } glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); SDL_GL_SwapWindow(ctx->window); #endif in_resize_render = false; return true; }; SDL_AddEventWatch(resizeWatcher, &resizeCtx); // Main loop bool running = true; bool needsRedraw = true; // P6: Start with a full redraw auto lastPeriodicFrame = std::chrono::steady_clock::now(); // Per-scale saved window sizes for DPI round-trips. // Each entry maps scale-percentage (100, 125, 150, 200, …) to the // physical pixel size the user had at that scale. When transitioning // to a scale we've visited before the stored size is restored, so // resizing on a 200% screen doesn't clobber the 100% size. std::map> savedSizeForScale; bool dpiResizePending = false; bool fontScaleResizePending = false; // Font-scale window reference: size at scale 1.0 (updated on user resize) float fontScaleRefFS = 0.0f; int fontScaleRefW = 0, fontScaleRefH = 0; // Track last known window size at the current DPI scale. // Updated on user-initiated resizes (not DPI auto-resizes). // When DISPLAY_SCALE_CHANGED fires, SDL_GetWindowSize() already // returns the auto-resized value, so we use these instead. int lastKnownW = 0, lastKnownH = 0; // Deferred resize: SDL_SetWindowSize may not stick when the user // is dragging the window between monitors (Windows modal drag loop // overrides SetWindowPos). We retry for several frames. int dpiTargetW = 0, dpiTargetH = 0; int dpiResizeRetries = 0; { float s = dragonx::ui::material::Typography::instance().getDpiScale(); int w = 0, h = 0; SDL_GetWindowSize(window, &w, &h); int pct = (int)lroundf(s * 100.0f); savedSizeForScale[pct] = {w, h}; lastKnownW = w; lastKnownH = h; } while (running) { // Deferred DPI resize: if a previous DPI transition set a target // size that SDL_SetWindowSize didn't achieve (e.g. because the // window was being dragged), retry until it sticks. if (dpiResizeRetries > 0) { int curW = 0, curH = 0; SDL_GetWindowSize(window, &curW, &curH); if (curW != dpiTargetW || curH != dpiTargetH) { dpiResizePending = true; SDL_SetWindowSize(window, dpiTargetW, dpiTargetH); DEBUG_LOGF(" Deferred resize retry %d: %dx%d -> %dx%d\n", dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH); } else { // Size matches — done dpiResizeRetries = 0; } --dpiResizeRetries; needsRedraw = true; } // P6: Idle rendering — sleep if nothing needs redrawing if (!needsRedraw) { // Wait up to 200ms for user input or wake event SDL_Event waitEvent; if (SDL_WaitEventTimeout(&waitEvent, 200)) { ImGui_ImplSDL3_ProcessEvent(&waitEvent); if (waitEvent.type == SDL_EVENT_QUIT) { app.beginShutdown(); } if (waitEvent.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && waitEvent.window.windowID == SDL_GetWindowID(window)) { app.beginShutdown(); } #ifdef DRAGONX_USE_DX11 if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED && waitEvent.window.windowID == SDL_GetWindowID(window)) { dx.resize(waitEvent.window.data1, waitEvent.window.data2); } #endif // Update saved size for current scale on user-initiated resize. // DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize) // are skipped so they don't overwrite the saved size. if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED && waitEvent.window.windowID == SDL_GetWindowID(window)) { float actualScale = SDL_GetWindowDisplayScale(window); float storedScale = dragonx::ui::material::Typography::instance().getDpiScale(); #ifdef __APPLE__ actualScale = storedScale; // macOS Retina: scales always match (both 1.0) #endif bool isDpiResize = dpiResizePending || std::abs(actualScale - storedScale) > 0.01f; if (dpiResizePending) dpiResizePending = false; if (!isDpiResize) { int pct = (int)lroundf(storedScale * 100.0f); savedSizeForScale[pct] = {waitEvent.window.data1, waitEvent.window.data2}; lastKnownW = waitEvent.window.data1; lastKnownH = waitEvent.window.data2; } } // Window restored from minimized — trigger immediate data refresh if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED && waitEvent.window.windowID == SDL_GetWindowID(window)) { app.refreshNow(); } // Handle DPI change that arrived while idle (same logic as poll loop) if (waitEvent.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED && waitEvent.window.windowID == SDL_GetWindowID(window)) { float newScale = SDL_GetWindowDisplayScale(window); if (newScale <= 0.0f) newScale = 1.0f; #ifdef __APPLE__ newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale #endif auto& typo = dragonx::ui::material::Typography::instance(); float oldScale = typo.getDpiScale(); if (std::abs(newScale - oldScale) > 0.01f) { float ratio = newScale / oldScale; DEBUG_LOGF("Display scale changed (idle): %.2f -> %.2f (ratio %.2f)\n", oldScale, newScale, ratio); ImGuiIO& io = ImGui::GetIO(); #ifdef DRAGONX_USE_DX11 g_borderlessDpi = newScale; #endif // Save the last known size (before auto-resize) under // the OLD scale. We use lastKnownW/H instead of // SDL_GetWindowSize() because by now WM_DPICHANGED // may have already auto-resized the window. { int oldPct = (int)lroundf(oldScale * 100.0f); savedSizeForScale[oldPct] = {lastKnownW, lastKnownH}; DEBUG_LOGF(" Saved %dx%d for scale %d%%\n", lastKnownW, lastKnownH, oldPct); } // Compute new physical size: if we've been to this // scale before, restore that exact size; otherwise // proportionally scale from the current size. int newPct = (int)lroundf(newScale * 100.0f); int newW, newH; auto it = savedSizeForScale.find(newPct); if (it != savedSizeForScale.end()) { newW = it->second.first; newH = it->second.second; } else { // Use lastKnownW/H (pre-auto-resize) instead of // SDL_GetWindowSize() which already reflects the // WM_DPICHANGED auto-resize. newW = (int)lroundf((float)lastKnownW * newScale / oldScale); newH = (int)lroundf((float)lastKnownH * newScale / oldScale); } // Clamp to the target display's work area so the // window doesn't overflow a smaller/higher-density monitor. SDL_DisplayID did = SDL_GetDisplayForWindow(window); if (did) { SDL_Rect usable; if (SDL_GetDisplayUsableBounds(did, &usable)) { newW = std::min(newW, usable.w); newH = std::min(newH, usable.h); } } dpiResizePending = true; SDL_SetWindowSize(window, newW, newH); lastKnownW = newW; lastKnownH = newH; // Arm deferred retry — the SetWindowSize above may // not stick if the user is mid-drag (modal loop). dpiTargetW = newW; dpiTargetH = newH; dpiResizeRetries = 30; // retry for ~30 frames SDL_SetWindowMinimumSize(window, (int)(1024 * newScale), (int)(720 * newScale)); typo.reload(io, newScale); ImGui::GetStyle() = ImGuiStyle(); dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui(); if (newScale > 1.01f) { ImGui::GetStyle().ScaleAllSizes(newScale); } } } needsRedraw = true; // Got an event — redraw } // Timeout expired: only redraw if there's actual work if (!needsRedraw) { auto now = std::chrono::steady_clock::now(); auto sinceLast = std::chrono::duration_cast( now - lastPeriodicFrame).count(); // Immediate triggers: async RPC results or visible notifications bool hasImmediateWork = app.hasPendingRPCResults() || app.hasTransactionSendProgress() || app.isTransactionRefreshInProgress() || dragonx::ui::Notifications::instance().hasActive(); // Periodic maintenance: fire refresh timers in app.update() // Low-spec: every 5s; normal: every 2s int periodMs = dragonx::ui::effects::isLowSpecMode() ? 5000 : 2000; if (hasImmediateWork || sinceLast >= periodMs) { needsRedraw = true; } else { continue; // Nothing to do — go back to sleep } } } // Poll remaining events SDL_Event event; // Deferred font-scale commit: smooth visual update during Alt+scroll, // atlas rebuild after scrolling stops (~200ms idle). static Uint64 fontScaleCommitTick = 0; // 0 = nothing pending while (SDL_PollEvent(&event)) { // Alt + scroll wheel: smooth font scale adjustment if (event.type == SDL_EVENT_MOUSE_WHEEL) { SDL_Keymod mods = SDL_GetModState(); if (mods & SDL_KMOD_ALT) { float step = 0.05f * event.wheel.y; float cur = dragonx::ui::Layout::userFontScale(); float next = std::max(1.0f, std::min(1.5f, cur + step)); if (next != cur) { // Smooth preview — no atlas rebuild yet dragonx::ui::Layout::setUserFontScaleVisual(next); // Schedule atlas rebuild after scrolling stops fontScaleCommitTick = SDL_GetTicks() + 200; } // Don't pass Alt+scroll to ImGui (would scroll windows) continue; } } ImGui_ImplSDL3_ProcessEvent(&event); if (event.type == SDL_EVENT_QUIT) { app.beginShutdown(); } if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window)) { app.beginShutdown(); } #ifdef DRAGONX_USE_DX11 // Handle window resize for DX11 swap chain if (event.type == SDL_EVENT_WINDOW_RESIZED && event.window.windowID == SDL_GetWindowID(window)) { dx.resize(event.window.data1, event.window.data2); } #endif // Update saved size for current scale on user-initiated resize. // DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize) // are skipped so they don't overwrite the saved size. if (event.type == SDL_EVENT_WINDOW_RESIZED && event.window.windowID == SDL_GetWindowID(window)) { float actualScale = SDL_GetWindowDisplayScale(window); float storedScale = dragonx::ui::material::Typography::instance().getDpiScale(); #ifdef __APPLE__ actualScale = storedScale; // macOS Retina: scales always match (both 1.0) #endif bool isDpiResize = dpiResizePending || std::abs(actualScale - storedScale) > 0.01f; if (dpiResizePending) dpiResizePending = false; bool isFSResize = fontScaleResizePending; if (fontScaleResizePending) fontScaleResizePending = false; if (!isDpiResize) { int pct = (int)lroundf(storedScale * 100.0f); savedSizeForScale[pct] = {event.window.data1, event.window.data2}; lastKnownW = event.window.data1; lastKnownH = event.window.data2; // User manually resized — update the font-scale reference // so the next scale change is relative to this size. if (!isFSResize) { float fs = dragonx::ui::Layout::userFontScale(); fontScaleRefW = (int)lroundf((float)event.window.data1 / fs); fontScaleRefH = (int)lroundf((float)event.window.data2 / fs); } } } // Window restored from minimized — trigger immediate data refresh if (event.type == SDL_EVENT_WINDOW_RESTORED && event.window.windowID == SDL_GetWindowID(window)) { app.refreshNow(); } // Handle DPI/display scale changes (e.g. window dragged to a // different-DPI monitor, or user changes Windows scaling) if (event.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED && event.window.windowID == SDL_GetWindowID(window)) { float newScale = SDL_GetWindowDisplayScale(window); if (newScale <= 0.0f) newScale = 1.0f; #ifdef __APPLE__ newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale #endif auto& typo = dragonx::ui::material::Typography::instance(); float oldScale = typo.getDpiScale(); if (std::abs(newScale - oldScale) > 0.01f) { float ratio = newScale / oldScale; DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n", oldScale, newScale, ratio); ImGuiIO& io = ImGui::GetIO(); #ifdef DRAGONX_USE_DX11 g_borderlessDpi = newScale; #endif // Save the last known size (before auto-resize) under // the OLD scale. We use lastKnownW/H instead of // SDL_GetWindowSize() because by now WM_DPICHANGED // may have already auto-resized the window. { int oldPct = (int)lroundf(oldScale * 100.0f); savedSizeForScale[oldPct] = {lastKnownW, lastKnownH}; DEBUG_LOGF(" Saved %dx%d for scale %d%%\n", lastKnownW, lastKnownH, oldPct); } // Compute new physical size: if we've been to this // scale before, restore that exact size; otherwise // proportionally scale from the current size. int newPct = (int)lroundf(newScale * 100.0f); int newW, newH; auto it = savedSizeForScale.find(newPct); if (it != savedSizeForScale.end()) { newW = it->second.first; newH = it->second.second; } else { // Use lastKnownW/H (pre-auto-resize) instead of // SDL_GetWindowSize() which already reflects the // WM_DPICHANGED auto-resize. newW = (int)lroundf((float)lastKnownW * newScale / oldScale); newH = (int)lroundf((float)lastKnownH * newScale / oldScale); } // Clamp to the target display's work area so the // window doesn't overflow a smaller/higher-density monitor. SDL_DisplayID did = SDL_GetDisplayForWindow(window); if (did) { SDL_Rect usable; if (SDL_GetDisplayUsableBounds(did, &usable)) { newW = std::min(newW, usable.w); newH = std::min(newH, usable.h); } } dpiResizePending = true; SDL_SetWindowSize(window, newW, newH); lastKnownW = newW; lastKnownH = newH; // Arm deferred retry — the SetWindowSize above may // not stick if the user is mid-drag (modal loop). dpiTargetW = newW; dpiTargetH = newH; dpiResizeRetries = 30; // retry for ~30 frames SDL_SetWindowMinimumSize(window, (int)(1024 * newScale), (int)(720 * newScale)); DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n", newW, newH, newScale, newPct); // 2) Rebuild font atlas at the new DPI scale typo.reload(io, newScale); // 3) Reset style → reapply base theme → scale by new DPI ImGui::GetStyle() = ImGuiStyle(); dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui(); if (newScale > 1.01f) { ImGui::GetStyle().ScaleAllSizes(newScale); } DEBUG_LOGF(" DPI transition complete\n"); } } } // Check if window is minimized if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) { // Still check shouldQuit while minimized to avoid hang if (app.shouldQuit()) { running = false; } SDL_Delay(10); continue; } // --- PerfLog: begin frame --- dragonx::util::PerfLog::instance().beginFrame(); // Commit deferred font-scale change once scrolling has stopped. if (fontScaleCommitTick != 0 && SDL_GetTicks() >= fontScaleCommitTick) { fontScaleCommitTick = 0; float fs = dragonx::ui::Layout::userFontScale(); dragonx::ui::Layout::setUserFontScale(fs); app.settings()->setFontScale(fs); app.settings()->save(); } // Pre-frame: font atlas rebuilds and schema hot-reload must // happen BEFORE NewFrame() because NewFrame() caches font ptrs. app.preFrame(); // Smooth font-scale: compensate visual font size via FontScaleMain // when the atlas hasn't been rebuilt yet (during slider drag). // After atlas rebuild, atlasScale == userFontScale so this is 1.0. { float userFS = dragonx::ui::Layout::userFontScale(); float atlasFS = dragonx::ui::Layout::fontAtlasScale(); if (atlasFS > 0.001f) ImGui::GetStyle().FontScaleMain = userFS / atlasFS; } // If font scale changed, resize window proportionally. // Reference size (at scale 1.0) is updated on user-initiated resizes // so the next scale change is relative to the current window, not a // stale snapshot. target = ref * curFS — no rounding drift. { float curFS = dragonx::ui::Layout::userFontScale(); if (fontScaleRefFS < 0.001f) { // First frame — record reference, no resize. fontScaleRefFS = curFS; int w, h; SDL_GetWindowSize(window, &w, &h); fontScaleRefW = (int)lroundf((float)w / curFS); fontScaleRefH = (int)lroundf((float)h / curFS); } if (std::fabs(curFS - fontScaleRefFS) > 0.001f) { int newW = (int)lroundf((float)fontScaleRefW * curFS); int newH = (int)lroundf((float)fontScaleRefH * curFS); // Clamp to display work area SDL_DisplayID did = SDL_GetDisplayForWindow(window); if (did) { SDL_Rect usable; if (SDL_GetDisplayUsableBounds(did, &usable)) { newW = std::min(newW, usable.w); newH = std::min(newH, usable.h); } } float hwDpi = dragonx::ui::Layout::rawDpiScale(); SDL_SetWindowMinimumSize(window, (int)(1024 * hwDpi), (int)(720 * hwDpi)); fontScaleResizePending = true; SDL_SetWindowSize(window, newW, newH); lastKnownW = newW; lastKnownH = newH; fontScaleRefFS = curFS; } } // Start the Dear ImGui frame PERF_BEGIN(_perfNewFrame); #ifdef DRAGONX_USE_DX11 ImGui_ImplDX11_NewFrame(); #else ImGui_ImplOpenGL3_NewFrame(); #endif ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); PERF_END("NewFrame", _perfNewFrame); PERF_BEGIN(_perfBackdrop); // Draw background image overlay across the entire viewport. // Uses theme-specified texture stretched to fill, with the // programmatic gradient as fallback when texture isn't available. { ImGuiViewport* vp = ImGui::GetMainViewport(); ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); ImVec2 p0 = vp->Pos; ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y); // Read the current gradient texture from the app each frame // so hot-reloaded / skin-changed images are picked up immediately. ImTextureID curGradTex = app.getGradientTexture(); // Window opacity: scale only background layer alpha so the // desktop shows through while UI stays fully opaque. float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f; bool lowSpec = dragonx::ui::effects::isLowSpecMode(); // Scale a single color's alpha channel by window opacity auto scaleAlpha = [&](ImU32 col) -> ImU32 { uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF; a = (uint8_t)(a * winOpacity); return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT); }; if (curGradTex != 0) { // Base color gradient underneath the texture — read from ui.toml const auto& S2 = dragonx::ui::schema::UI(); ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200)); ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200)); if (lowSpec) { // Low-spec: skip texture sampling — just draw the base gradient. bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop), scaleAlpha(baseBottom), scaleAlpha(baseBottom)); } else { int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size; if (tintAlpha <= 0) tintAlpha = 140; // Scale background gradient + texture tint alpha by // window opacity — only affects the backdrop, not UI. bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop), scaleAlpha(baseBottom), scaleAlpha(baseBottom)); int scaledTintAlpha = (int)(tintAlpha * winOpacity); ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha); bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol); } } else if (backdrop_active) { // Programmatic gradient tint (fallback when texture unavailable) const auto& S = dragonx::ui::schema::UI(); auto bde = [&](const char* key, float fb) { float v = S.drawElement("backdrop", key).size; return v >= 0 ? v : fb; }; ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12), (int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity)); ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8), (int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity)); bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom); } // Insert acrylic capture callback BEFORE the noise overlay // so the noise grain is not captured and blurred into the // glass cards (the noise should only be a visual overlay). // Skip in low-spec mode — acrylic is disabled so the FBO // capture/blur callback is unnecessary GPU work. if (!lowSpec) { auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback(); if (captureCb) { bgDL->AddCallback(captureCb, nullptr); bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr); } } // WindowBg alpha stays at its base theme value — NOT scaled // by window opacity. Only the background gradient/texture // layers fade so UI elements remain fully readable. if (backdrop_active || curGradTex != 0) { const auto& S3 = dragonx::ui::schema::UI(); float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f); ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha; } } PERF_END("Backdrop", _perfBackdrop); // Begin acrylic frame (update viewport dimensions) // Skip in low-spec mode — acrylic is disabled, no need to track FBO sizes ImGuiIO& io = ImGui::GetIO(); if (!dragonx::ui::effects::isLowSpecMode()) { dragonx::ui::effects::ImGuiAcrylic::BeginFrame( (int)io.DisplaySize.x, (int)io.DisplaySize.y); } // Update theme visual effects timing dragonx::ui::effects::ThemeEffects::instance().beginFrame(); // Propagate window opacity to theme effects so they fade with the // backdrop while UI elements (cards, text, buttons) stay opaque. { float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f; dragonx::ui::effects::ThemeEffects::instance().setBackgroundOpacity(wo); } // Reset smooth-scroll animation flag — ApplySmoothScroll() calls // during this frame will set it back to true if still interpolating. dragonx::ui::material::SmoothScrollAnimating() = false; // Always render normal UI try { PERF_BEGIN(_perfUpdate); app.update(); PERF_END("AppUpdate", _perfUpdate); } catch (const std::exception& e) { DEBUG_LOGF("[Main] app.update() threw: %s\n", e.what()); } catch (...) { DEBUG_LOGF("[Main] app.update() threw unknown exception\n"); } // Background capture is now handled by the draw callback // inserted into the BackgroundDrawList above. It fires // during RenderDrawData() at exactly the right moment. ImGuiErrorRecoveryState erState; ImGui::ErrorRecoveryStoreState(&erState); try { PERF_BEGIN(_perfRender); app.render(); PERF_END("AppRender", _perfRender); } catch (const std::exception& e) { DEBUG_LOGF("[Main] app.render() threw: %s\n", e.what()); ImGui::ErrorRecoveryTryToRecoverState(&erState); } catch (...) { DEBUG_LOGF("[Main] app.render() threw unknown exception\n"); ImGui::ErrorRecoveryTryToRecoverState(&erState); } // Draw shutdown overlay on top of everything when shutting down if (app.isShuttingDown()) { app.renderShutdownScreen(); } #ifdef DRAGONX_USE_DX11 // Borderless title bar: window controls + drag handling renderBorderlessControls(window, nativeHwnd, app); #endif // Global cursor: show hand cursor when hovering any clickable widget // (ImGui buttons don't change the cursor by default) // Persist cursor for 2 extra frames to avoid flicker on Windows // where g.HoveredId may clear briefly between frames. { ImGuiContext& g = *GImGui; static ImGuiMouseCursor s_prevCursor = ImGuiMouseCursor_Arrow; static int s_holdFrames = 0; bool isInputField = (g.InputTextState.ID != 0 && g.InputTextState.ID == g.HoveredId); if (g.MouseCursor == ImGuiMouseCursor_Arrow && g.HoveredId != 0 && !(g.HoveredIdIsDisabled) && !isInputField) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); s_prevCursor = ImGuiMouseCursor_Hand; s_holdFrames = 2; } else if (s_holdFrames > 0 && g.MouseCursor == ImGuiMouseCursor_Arrow) { // Hold previous cursor to avoid single-frame revert flicker ImGui::SetMouseCursor(s_prevCursor); s_holdFrames--; } else { s_prevCursor = g.MouseCursor; s_holdFrames = 0; } } // Rendering PERF_BEGIN(_perfImRender); ImGui::Render(); PERF_END("ImGui::Render", _perfImRender); // Apply cursor immediately via SDL so it takes effect this frame. // ImGui_ImplSDL3_UpdateMouseCursor() runs at the START of the // next frame, creating a 1-frame lag. By calling SDL_SetCursor() // here we eliminate that delay and any flicker. { static SDL_Cursor* sdlCursors[ImGuiMouseCursor_COUNT] = {}; static bool sdlCursorsInit = false; if (!sdlCursorsInit) { sdlCursors[ImGuiMouseCursor_Arrow] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); sdlCursors[ImGuiMouseCursor_TextInput] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT); sdlCursors[ImGuiMouseCursor_ResizeAll] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE); sdlCursors[ImGuiMouseCursor_ResizeNS] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NS_RESIZE); sdlCursors[ImGuiMouseCursor_ResizeEW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_EW_RESIZE); sdlCursors[ImGuiMouseCursor_ResizeNESW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NESW_RESIZE); sdlCursors[ImGuiMouseCursor_ResizeNWSE] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE); sdlCursors[ImGuiMouseCursor_Hand] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER); sdlCursors[ImGuiMouseCursor_NotAllowed] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED); sdlCursorsInit = true; } ImGuiMouseCursor cur = ImGui::GetMouseCursor(); if (cur >= 0 && cur < ImGuiMouseCursor_COUNT && sdlCursors[cur]) SDL_SetCursor(sdlCursors[cur]); } PERF_BEGIN(_perfPresent); #ifdef DRAGONX_USE_DX11 // Ensure swap chain matches current window size. // SDL_EVENT_WINDOW_RESIZED may be delayed during live resize // (Windows runs a modal loop while the user drags the border). dx.ensureSize(); // DX11: Set render target and clear with transparent black for DWM ID3D11RenderTargetView* rtv = dx.renderTargetView(); dx.deviceContext()->OMSetRenderTargets(1, &rtv, nullptr); { // When the background is fully opaque (low-spec mode or // window_opacity >= 1.0), clear with alpha 1.0 so the // compositor/DWM skips expensive blur compositing. float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f; bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f; bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0) && !opaqueBackground; dx.clear(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f); } ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); // Update and Render additional Platform Windows (multi-viewport) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); } dx.present(1); #else glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); { // When the background is fully opaque (low-spec mode or // window_opacity >= 1.0), clear with alpha 1.0 so the // compositor skips alpha blending against the desktop. float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f; bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f; bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0) && !opaqueBackground; glClearColor(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f); } glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); // Update and Render additional Platform Windows (multi-viewport) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow(); SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); SDL_GL_MakeCurrent(backup_current_window, backup_current_context); } SDL_GL_SwapWindow(window); #endif PERF_END("Present", _perfPresent); // --- PerfLog: end frame --- dragonx::util::PerfLog::instance().endFrame(); // Exit when shutdown is complete if (app.shouldQuit()) { running = false; } // Periodic log flush so crash diagnostics are visible { static auto lastFlush = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - lastFlush).count() >= 5) { fflush(stdout); fflush(stderr); lastFlush = now; } } // Track last render for idle periodic timer lastPeriodicFrame = std::chrono::steady_clock::now(); // P6: Determine if next frame needs redraw or can idle { ImGuiContext& g = *GImGui; // Detect actual user interaction — NOT WantCaptureMouse/Keyboard // which are just "ImGui has windows that accept input" (always true). bool mouseMoving = g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f; bool mouseClicked = false; for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) mouseClicked |= g.IO.MouseDown[i]; bool scrolling = g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f; bool typing = g.IO.InputQueueCharacters.Size > 0; bool widgetActive = g.ActiveId != 0 || g.MovingWindow != nullptr; bool uiActive = mouseMoving || mouseClicked || scrolling || typing || widgetActive; // When the background is fully opaque, backdrop doesn't // need continuous frames — let the app idle and save power. float wo2 = app.settings() ? app.settings()->getWindowOpacity() : 1.0f; bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo2 >= 0.99f; bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0) && !opaqueBackground; bool animating = app.isShuttingDown() || backdropNeedsFrames || app.hasTransactionSendProgress() || app.isTransactionRefreshInProgress() || dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation() || dragonx::ui::Notifications::instance().hasActive() || dragonx::ui::material::SmoothScrollAnimating(); // If nothing is happening, allow the next iteration to idle needsRedraw = uiActive || animating; } } // Remove resize event watcher SDL_RemoveEventWatch(resizeWatcher, &resizeCtx); // Save window size to settings before shutdown if (app.settings()) { int curW = 0, curH = 0; SDL_GetWindowSize(window, &curW, &curH); float s = dragonx::ui::material::Typography::instance().getDpiScale(); int logW = (int)lroundf((float)curW / s); int logH = (int)lroundf((float)curH / s); if (logW >= 1024 && logH >= 720) { app.settings()->setWindowSize(logW, logH); app.settings()->save(); } } // Hide the window immediately so the user perceives the app as closed // while background cleanup (thread joins, RPC disconnect) continues. SDL_HideWindow(window); // Watchdog: if cleanup takes too long the process lingers without a // window, showing up as a "Background Service" in Task Manager. // Force-exit after timeout — all critical state (settings, daemon // stop) was handled in beginShutdown(). // Allow enough time for the daemon to shut down gracefully (up to // 10s in stop()) plus RPC disconnect overhead. std::thread([]() { std::this_thread::sleep_for(std::chrono::seconds(15)); fflush(stdout); fflush(stderr); _Exit(0); }).detach(); // Final cleanup (daemon already stopped by beginShutdown) app.shutdown(); #ifdef DRAGONX_USE_DX11 Shutdown(window, dx); #else Shutdown(window, gl_context); #endif // Explicitly release the single-instance lock before exit so a new // instance can start immediately. g_single_instance.unlock(); // Force-terminate the process. All important cleanup (daemon stop, // settings save, RPC disconnect, SDL teardown) has completed above. // On Windows with mingw-w64 POSIX threads, normal CRT cleanup // deadlocks waiting for detached pthreads. On Linux, static // destructors and atexit handlers can also block. _Exit() bypasses // all of that. fflush(stdout); fflush(stderr); _Exit(0); return 0; } static bool InitSDL() { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) { fprintf(stderr, "Error: SDL_Init(): %s\n", SDL_GetError()); return false; } return true; } // Persistent storage for IniFilename — must outlive ImGui context static std::string s_iniPath; #ifdef DRAGONX_USE_DX11 static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx) { // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); // Save imgui.ini to ObsidianDragon config directory s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string(); io.IniFilename = s_iniPath.c_str(); // Enable keyboard navigation io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable gamepad navigation io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable multi-viewport (windows can float outside main window) io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Setup Platform/Renderer backends ImGui_ImplSDL3_InitForD3D(window); ImGui_ImplDX11_Init(dx.device(), dx.deviceContext()); // Query display DPI scale (e.g. 1.5 for 150% Windows scaling) float dpiScale = SDL_GetWindowDisplayScale(window); if (dpiScale <= 0.0f) dpiScale = 1.0f; DEBUG_LOGF("Display scale: %.2f\n", dpiScale); // Load Material Design typography system with DPI awareness if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) { DEBUG_LOGF("Warning: Could not load typography system, using defaults\n"); io.Fonts->AddFontDefault(); } return true; } static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx) { ImGui_ImplDX11_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); dx.shutdown(); // Save native HWND before SDL releases its reference. // SDL marks externally-provided HWNDs as SDL_WINDOW_EXTERNAL and // does NOT call DestroyWindow -- we must do it ourselves. HWND hwnd = nullptr; if (window) { hwnd = (HWND)SDL_GetPointerProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); SDL_DestroyWindow(window); } if (hwnd) { DestroyWindow(hwnd); } SDL_Quit(); } #else // OpenGL path static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context) { // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); // Save imgui.ini to ObsidianDragon config directory s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string(); io.IniFilename = s_iniPath.c_str(); // Enable keyboard navigation io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable gamepad navigation io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable multi-viewport (windows can float outside main window) io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Setup Platform/Renderer backends ImGui_ImplSDL3_InitForOpenGL(window, gl_context); #ifdef __APPLE__ ImGui_ImplOpenGL3_Init("#version 150"); #else ImGui_ImplOpenGL3_Init("#version 130"); #endif // Query display DPI scale (e.g. 1.5 for 150% scaling) float dpiScale = SDL_GetWindowDisplayScale(window); if (dpiScale <= 0.0f) dpiScale = 1.0f; #ifdef __APPLE__ // On macOS with SDL_WINDOW_HIGH_PIXEL_DENSITY, ImGui handles Retina // resolution automatically via io.DisplayFramebufferScale. Window // coordinates are already in logical points, so we must NOT also // scale fonts and style sizes by the content scale — that would // double everything. dpiScale = 1.0f; #endif DEBUG_LOGF("Display scale: %.2f\n", dpiScale); // Load Material Design typography system with DPI awareness // This loads Ubuntu fonts in Light, Regular, and Medium weights // at all sizes needed for the type scale (H1-H6, Body1-2, etc.) if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) { DEBUG_LOGF("Warning: Could not load typography system, using defaults\n"); io.Fonts->AddFontDefault(); } return true; } static void Shutdown(SDL_Window* window, SDL_GLContext gl_context) { // Shutdown acrylic effects before destroying GL context dragonx::ui::effects::ImGuiAcrylic::Shutdown(); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); if (gl_context) { SDL_GL_DestroyContext(gl_context); } if (window) { SDL_DestroyWindow(window); } SDL_Quit(); } #endif // DRAGONX_USE_DX11