#include "app_base.h" #include "version_generated.h" #include "imgui.h" #include "imgui_internal.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "implot.h" #include "implot3d.h" #include "core/tokens.h" #include "core/icon_font.h" #include "core/app_settings.h" #include "core/app_about.h" #include "core/app_menubar.h" #include "core/fps_overlay.h" #include "core/logger.h" #include "core/log_window.h" #include "core/layout_storage.h" #include "gfx/gl_loader.h" #include "app_modules.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include // GET_X_LPARAM / GET_Y_LPARAM #define GLFW_EXPOSE_NATIVE_WIN32 #include #else #include #endif #ifdef TRACY_ENABLE #include "tracy/Tracy.hpp" #endif static void glfw_error_callback(int error, const char* description) { fprintf(stderr, "GLFW Error %d: %s\n", error, description); } #ifdef _WIN32 // AltSnap (and other external window movers — tiling WMs, snap-assist) bracket // their drag with WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE messages but, unlike the // native title-bar drag, do NOT block the application thread inside the // modal DefWindowProc move loop. Result: the app keeps rendering and swapping // buffers while the OS posts SetWindowPos(SWP_ASYNCWINDOWPOS) calls, racing // the framebuffer presentation against the live window position and producing // the visible jitter / "grab and release" flicker the user reports. // // Native title-bar drag has no jitter precisely because Windows enters the // modal sizemove loop and the app stops drawing — the DWM compositor moves // the existing buffer pixels. We replicate that contract: while sizemove is // active, skip render + glfwSwapBuffers, only pump the message queue. As soon // as WM_EXITSIZEMOVE arrives, normal rendering resumes. // // IMPORTANT: the subclass must cover EVERY HWND owned by the process — main // window AND every secondary viewport platform window the ImGui GLFW backend // creates when the user drags a panel outside the main. Otherwise AltSnap on // a secondary HWND would not be observed, the main loop would keep rendering, // and the visible jitter would return on that panel. g_in_sizemove stays // global on purpose: any external move on ANY of our HWNDs pauses the whole // render pipeline, exactly like the native title-bar drag contract. static std::atomic g_in_sizemove{false}; // Test observability — monotonic counters. fn::internal exposes accessors. static std::atomic g_sizemove_enter_count{0}; static std::atomic g_alt_rmb_resize_count{0}; static std::atomic g_alt_lmb_move_count{0}; // Test hook — bypasses GetAsyncKeyState(VK_MENU) so headless tests can drive // the Alt+RMB / Alt+LMB paths without UI-access for keybd_event. static std::atomic g_force_alt_for_test{false}; // Diagnostic: every WM_RBUTTONDOWN this subclass sees (Alt-or-not). Used to // distinguish "message never arrived" from "Alt check failed". static std::atomic g_rbuttondown_seen_count{0}; // Accessed only from the main (render) thread. Map value is the original // WNDPROC for that HWND so we can restore and chain CallWindowProcW. static std::unordered_map g_subclassed; // Pick the WMSZ_* direction whose modal resize will feel natural depending // on which quadrant of the client rect the cursor is in. Matches AltSnap's // quadrant rule (top-left -> shrink toward top-left, etc.). static int alt_rmb_resize_direction(HWND hwnd, int client_x, int client_y) { RECT rc{}; if (!GetClientRect(hwnd, &rc)) return 8 /* WMSZ_BOTTOMRIGHT */; int cx = (rc.right - rc.left) / 2; int cy = (rc.bottom - rc.top) / 2; bool top = (client_y < cy); bool left = (client_x < cx); if (top && left) return 4; // WMSZ_TOPLEFT if (top && !left) return 5; // WMSZ_TOPRIGHT if (!top && left) return 7; // WMSZ_BOTTOMLEFT return 8; // WMSZ_BOTTOMRIGHT } static LRESULT CALLBACK fn_subclass_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_ENTERSIZEMOVE: g_in_sizemove.store(true, std::memory_order_release); g_sizemove_enter_count.fetch_add(1, std::memory_order_acq_rel); break; case WM_EXITSIZEMOVE: g_in_sizemove.store(false, std::memory_order_release); break; case WM_LBUTTONDOWN: // Alt + LMB anywhere on the window initiates a native modal MOVE // via WM_SYSCOMMAND, SC_MOVE | HTCAPTION. Same pattern as our // Alt+RMB resize: ReleaseCapture, post the syscommand, return 0 // to consume the click. Windows then drives a normal move modal // (DefWindowProc blocks the thread) and our existing // WM_ENTERSIZEMOVE gate pauses render so there's no jitter. { bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire); if (alt_real || alt_test) { g_alt_lmb_move_count.fetch_add(1, std::memory_order_acq_rel); if (alt_test) { // Test mode: skip SC_MOVE post to keep the harness // out of Windows' modal move loop. return 0; } ReleaseCapture(); PostMessageW(hwnd, WM_SYSCOMMAND, (WPARAM)(0xF010 /* SC_MOVE */ | 2 /* HTCAPTION */), 0); return 0; } } break; case WM_RBUTTONDOWN: g_rbuttondown_seen_count.fetch_add(1, std::memory_order_acq_rel); // Alt + RMB anywhere on the window initiates a native modal // resize. Direction is chosen by cursor quadrant relative to // window center so dragging "feels" like grabbing the nearest // corner. ReleaseCapture is required before WM_SYSCOMMAND // SC_SIZE so the modal loop can take input. The subsequent // WM_ENTERSIZEMOVE bracket is observed above, so render is // gated for free and no jitter appears. { bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire); if (alt_real || alt_test) { int cx = GET_X_LPARAM(lp); int cy = GET_Y_LPARAM(lp); int dir = alt_rmb_resize_direction(hwnd, cx, cy); g_alt_rmb_resize_count.fetch_add(1, std::memory_order_acq_rel); if (alt_test) { // Test mode: skip the SC_SIZE post to keep the modal // out of the headless test harness. The counter is // sufficient to verify the path was taken; the modal // entry is exercised in the real-input manual test. return 0; } ReleaseCapture(); PostMessageW(hwnd, WM_SYSCOMMAND, (WPARAM)(0xF000 /* SC_SIZE */ | dir), 0); return 0; // consume so ImGui doesn't see a right-click } } break; default: break; } auto it = g_subclassed.find(hwnd); WNDPROC orig = (it != g_subclassed.end()) ? it->second : nullptr; if (orig) return CallWindowProcW(orig, hwnd, msg, wp, lp); return DefWindowProcW(hwnd, msg, wp, lp); } static void install_sizemove_subclass_hwnd(HWND hwnd) { if (!hwnd) return; if (g_subclassed.find(hwnd) != g_subclassed.end()) return; // idempotent WNDPROC orig = (WNDPROC)SetWindowLongPtrW( hwnd, GWLP_WNDPROC, (LONG_PTR)fn_subclass_wndproc); g_subclassed[hwnd] = orig; } // Resource ID generado por cpp/CMakeLists.txt en _appicon.rc: // 101 ICON "/appicon.ico" // Si la app no tiene appicon.ico el .rc no se genera y LoadImageW devuelve // NULL — no error visible, los HWND quedan con el icono GLFW por defecto. #define FN_APP_ICON_RES_ID 101 // Carga el icono embebido al tamaño OS-recomendado para small (title bar) y // big (Alt+Tab / taskbar). LR_SHARED -> Windows gestiona el handle; no hay // que DestroyIcon. Cacheado por HMODULE+ID+size. static HICON load_app_icon(int cx, int cy) { HMODULE mod = GetModuleHandleW(nullptr); return (HICON)LoadImageW(mod, MAKEINTRESOURCEW(FN_APP_ICON_RES_ID), IMAGE_ICON, cx, cy, LR_SHARED | LR_DEFAULTCOLOR); } // Adjunta el icono embebido al HWND: // WM_SETICON ICON_SMALL -> title bar (16x16) y Alt+Tab small variant. // WM_SETICON ICON_BIG -> taskbar (32x32) y Alt+Tab big variant. // SetClassLongPtrW propaga el icono al WNDCLASS para que nuevos HWNDs de la // misma clase lo hereden (no critico — el per-frame scan ya cubre cada // viewport secundario via su HWND propio, que puede tener WNDCLASS distinta). static std::unordered_set g_icon_attached; static void attach_app_icon_to_hwnd(HWND hwnd) { if (!hwnd) return; if (g_icon_attached.count(hwnd)) return; // idempotent HICON hSmall = load_app_icon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); HICON hBig = load_app_icon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); if (!hSmall && !hBig) return; // no appicon.ico embebido — nada que hacer if (hSmall) SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); if (hBig) SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); if (hSmall) SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall); if (hBig) SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig); g_icon_attached.insert(hwnd); } static void prune_dead_icon_attached() { for (auto it = g_icon_attached.begin(); it != g_icon_attached.end();) { if (!IsWindow(*it)) it = g_icon_attached.erase(it); else ++it; } } // Pinta la title bar (caption + bordes) en oscuro via DWM, para que el // header del SO no se quede blanco mientras el cliente es dark. DWM la pinta // el OS, no GLFW/ImGui — sin esta llamada queda en blanco aunque el resto // este oscuro. // // Carga dwmapi.dll dinamicamente: evita anadir la dep al toolchain. Si la // DLL/atributo no existe (Win10 < 1809), no hace nada — silencioso. // Attr 20 = DWMWA_USE_IMMERSIVE_DARK_MODE (Win11 / Win10 >= build 18985). // Attr 19 = nombre antiguo (Win10 1809..18984). Probar 20 primero, fallback 19. // Force repaint con SWP_FRAMECHANGED — la ventana ya fue mostrada por GLFW // antes de que lleguemos aqui. typedef HRESULT (WINAPI *PFN_DwmSetWindowAttribute)(HWND, DWORD, LPCVOID, DWORD); static std::unordered_set g_dark_titlebar_applied; static void attach_dark_titlebar_to_hwnd(HWND hwnd, bool dark) { if (!hwnd) return; if (g_dark_titlebar_applied.count(hwnd)) return; // idempotent static HMODULE h_dwmapi = LoadLibraryW(L"dwmapi.dll"); if (!h_dwmapi) return; static auto p_set = (PFN_DwmSetWindowAttribute)GetProcAddress(h_dwmapi, "DwmSetWindowAttribute"); if (!p_set) return; BOOL value = dark ? TRUE : FALSE; HRESULT hr = p_set(hwnd, 20 /* DWMWA_USE_IMMERSIVE_DARK_MODE */, &value, sizeof(value)); if (FAILED(hr)) { p_set(hwnd, 19 /* legacy name on Win10 1809..18984 */, &value, sizeof(value)); } SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); g_dark_titlebar_applied.insert(hwnd); } static void prune_dead_dark_titlebar() { for (auto it = g_dark_titlebar_applied.begin(); it != g_dark_titlebar_applied.end();) { if (!IsWindow(*it)) it = g_dark_titlebar_applied.erase(it); else ++it; } } static void install_sizemove_subclass(GLFWwindow* w) { if (!w) return; install_sizemove_subclass_hwnd(glfwGetWin32Window(w)); } // Reap stale entries: when a secondary viewport is destroyed (panel re-docked // back into main), the GLFW backend calls glfwDestroyWindow and the HWND is // invalidated. Drop those entries so we don't hold dangling pointers and so a // fresh HWND at the same address gets re-subclassed cleanly. static void prune_dead_subclassed() { for (auto it = g_subclassed.begin(); it != g_subclassed.end();) { if (!IsWindow(it->first)) it = g_subclassed.erase(it); else ++it; } } static void uninstall_sizemove_subclass_all() { for (auto& kv : g_subclassed) { if (IsWindow(kv.first) && kv.second) { SetWindowLongPtrW(kv.first, GWLP_WNDPROC, (LONG_PTR)kv.second); } } g_subclassed.clear(); } static inline bool external_sizemove_active() { return g_in_sizemove.load(std::memory_order_acquire); } #else static inline bool external_sizemove_active() { return false; } #endif namespace fn { // ============================================================================ // Local files // ============================================================================ namespace { std::string compute_exe_dir() { #ifdef _WIN32 wchar_t buf[MAX_PATH * 2]; DWORD n = GetModuleFileNameW(nullptr, buf, (DWORD)(sizeof(buf) / sizeof(buf[0]))); if (n == 0 || n >= sizeof(buf)/sizeof(buf[0])) return ""; int u8n = WideCharToMultiByte(CP_UTF8, 0, buf, (int)n, nullptr, 0, nullptr, nullptr); std::string out(u8n, 0); WideCharToMultiByte(CP_UTF8, 0, buf, (int)n, out.data(), u8n, nullptr, nullptr); size_t slash = out.find_last_of("/\\"); return (slash == std::string::npos) ? "" : out.substr(0, slash); #else char buf[4096]; ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); if (n <= 0) return ""; buf[n] = 0; std::string out(buf); size_t slash = out.find_last_of('/'); return (slash == std::string::npos) ? "" : out.substr(0, slash); #endif } const std::string& exe_dir_ref() { static std::string cached = compute_exe_dir(); return cached; } const std::string& local_dir_ref() { static std::string cached; static bool inited = false; if (inited) return cached; const std::string& edir = exe_dir_ref(); if (edir.empty()) { cached = "local_files"; // fallback: relativo al cwd } else { cached = edir + "/local_files"; } std::error_code ec; std::filesystem::create_directories(cached, ec); inited = true; return cached; } } // namespace const char* exe_dir() { return exe_dir_ref().c_str(); } const char* local_dir() { return local_dir_ref().c_str(); } const char* local_path(const char* name) { static thread_local std::string buf; buf = local_dir_ref(); if (name && *name) { if (!buf.empty() && buf.back() != '/' && buf.back() != '\\') buf += '/'; buf += name; } return buf.c_str(); } namespace { const std::string& asset_dir_ref() { static std::string cached; static bool inited = false; if (inited) return cached; const std::string& edir = exe_dir_ref(); cached = edir.empty() ? std::string("assets") : edir + "/assets"; inited = true; return cached; } } // namespace const char* asset_dir() { return asset_dir_ref().c_str(); } const char* asset_path(const char* name) { static thread_local std::string buf; buf = asset_dir_ref(); if (name && *name) { if (!buf.empty() && buf.back() != '/' && buf.back() != '\\') buf += '/'; buf += name; } return buf.c_str(); } void migrate_to_local_files(const char* const* names, std::size_t n) { if (!names || n == 0) return; const std::string& ldir = local_dir_ref(); const std::string& edir = exe_dir_ref(); for (std::size_t i = 0; i < n; ++i) { const char* name = names[i]; if (!name || !*name) continue; std::string dst = ldir + "/" + name; struct stat st{}; if (::stat(dst.c_str(), &st) == 0) continue; // ya existe en local // Buscar en exe_dir y en cwd. Mover el primero que aparezca. std::string cands[] = { edir.empty() ? std::string() : (edir + "/" + name), std::string(name), }; for (const auto& src : cands) { if (src.empty() || src == dst) continue; if (::stat(src.c_str(), &st) != 0) continue; std::error_code ec; std::filesystem::rename(src, dst, ec); if (ec) { // Cross-device o permisos — fallback a copy + remove. std::filesystem::copy(src, dst, std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, ec); if (!ec) std::filesystem::remove_all(src, ec); } std::fprintf(stdout, "[local_files] migrado: %s -> %s\n", src.c_str(), dst.c_str()); break; } } } const char* framework_version() { return FN_MODULE_FRAMEWORK_VERSION; } const char* framework_description() { return FN_MODULE_FRAMEWORK_DESCRIPTION; } // ---------------------------------------------------------------------------- // Header badge overlay — identidad por app en viewports secundarios. // ---------------------------------------------------------------------------- // Cuando una app C++ tiene N panels y el usuario arrastra varios fuera del // main window, cada panel se convierte en su propio OS viewport. Sin marcas // visuales adicionales, si tienes 3 apps abiertas a la vez no sabes de cual // viene cada panel flotante. Este overlay dibuja un cuadrado redondeado de // ~18px con la inicial de la app en la esquina top-left de la title bar de // cada viewport secundario. Solo en secundarios — el main ya tiene icono // del SO en titlebar/taskbar (attach_app_icon_to_hwnd). // // Filosofia: // - Defaults producen identidad util sin tocar la app (color hash-derivado // desde about.name, glyph = primera letra). // - Apps con icon.accent en su app.md pueden pasar el mismo hex para // coherencia con App Hub. // - ForegroundDrawList: dibuja por encima del titlebar de ImGui sin // necesidad de envolver Begin() ni hookear el render del titulo. // ---------------------------------------------------------------------------- // Parsea "#RRGGBB" o "RRGGBB" a ImU32 ABGR. Devuelve 0 si invalido. static ImU32 parse_hex_color_abgr(const char* s) { if (!s || !*s) return 0; if (*s == '#') ++s; auto h = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; int v[6]; for (int i = 0; i < 6; ++i) { v[i] = h(s[i]); if (v[i] < 0) return 0; } int r = (v[0] << 4) | v[1]; int g = (v[2] << 4) | v[3]; int b = (v[4] << 4) | v[5]; return IM_COL32(r, g, b, 255); } // Hash-derivado: FNV-1a 32-bit -> H en [0,360), S=0.58, V=0.78 -> ImU32 ABGR. // Estable por nombre, distribuye razonablemente entre N apps. static ImU32 derive_color_from_name(const char* name) { if (!name || !*name) name = "fn_registry"; unsigned h = 2166136261u; for (const char* p = name; *p; ++p) { h ^= (unsigned char)*p; h *= 16777619u; } float hue = (float)(h % 360u); float s = 0.58f, v = 0.78f; float c = v * s; float hp = hue / 60.0f; float x = c * (1.0f - std::fabs(std::fmod(hp, 2.0f) - 1.0f)); float r=0, g=0, b=0; if (hp < 1) { r=c; g=x; } else if (hp < 2) { r=x; g=c; } else if (hp < 3) { g=c; b=x; } else if (hp < 4) { g=x; b=c; } else if (hp < 5) { r=x; b=c; } else { r=c; b=x; } float m = v - c; int R = (int)((r + m) * 255.0f + 0.5f); int G = (int)((g + m) * 255.0f + 0.5f); int B = (int)((b + m) * 255.0f + 0.5f); return IM_COL32(R, G, B, 255); } // Decide string a renderizar como glyph. Devuelve puntero a buffer estatico // thread-local cuando hace falta normalizar la primera letra del nombre. static const char* resolve_badge_glyph(const AppConfig& cfg) { const char* g = cfg.header_badge.glyph; if (g && *g) return g; static thread_local char letter[8] = {0}; const char* nm = (cfg.about.name && *cfg.about.name) ? cfg.about.name : cfg.title; char first = (nm && *nm) ? nm[0] : '?'; if (first >= 'a' && first <= 'z') first = (char)(first - 'a' + 'A'); letter[0] = first; letter[1] = '\0'; return letter; } // Color final con precedencia: // 1) Override explicito en cfg.header_badge.accent_hex (main.cpp) // 2) Codegen extern fn::app_header_accent_hex (icon.accent del app.md) // 3) Hash-derived desde about.name (siempre estable, da identidad gratis) static ImU32 resolve_badge_color(const AppConfig& cfg) { ImU32 c = parse_hex_color_abgr(cfg.header_badge.accent_hex); if (c != 0) return c; c = parse_hex_color_abgr(app_header_accent_hex); if (c != 0) return c; const char* nm = (cfg.about.name && *cfg.about.name) ? cfg.about.name : cfg.title; return derive_color_from_name(nm); } // Textura GL del icono de la app — extraida del HICON embebido en el .exe // (resource ID 101 generado por add_imgui_app desde appicon.ico). Cargada // perezosamente al primer frame y reutilizada en cada draw. 0 = no disponible. static GLuint g_app_icon_texture = 0; static int g_app_icon_size = 0; #ifdef _WIN32 // Carga el icono embebido a 32x32 y sube como textura GL RGBA8. Linear // filtering para que escalar 32->18 sea suave. Devuelve 0 si falla. static GLuint upload_hicon_to_gl_texture() { const int sz = 32; HICON hicon = (HICON)LoadImageW(GetModuleHandleW(nullptr), MAKEINTRESOURCEW(FN_APP_ICON_RES_ID), IMAGE_ICON, sz, sz, LR_DEFAULTCOLOR); if (!hicon) return 0; ICONINFO ii{}; if (!GetIconInfo(hicon, &ii)) { DestroyIcon(hicon); return 0; } HDC hdc = CreateCompatibleDC(nullptr); BITMAPINFO bmi{}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = sz; bmi.bmiHeader.biHeight = -sz; // top-down bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; std::vector pixels(sz * sz * 4, 0); int ok = GetDIBits(hdc, ii.hbmColor, 0, sz, pixels.data(), &bmi, DIB_RGB_COLORS); DeleteDC(hdc); if (ii.hbmColor) DeleteObject(ii.hbmColor); if (ii.hbmMask) DeleteObject(ii.hbmMask); DestroyIcon(hicon); if (ok == 0) return 0; // BGRA -> RGBA (Windows DIB es BGRA). for (int i = 0; i < sz * sz; ++i) { unsigned char b = pixels[i*4 + 0]; unsigned char r = pixels[i*4 + 2]; pixels[i*4 + 0] = r; pixels[i*4 + 2] = b; } GLuint tex = 0; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sz, sz, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); return tex; } #endif // Itera todas las ventanas ImGui y pinta el badge en la title bar de las que // viven en un viewport secundario (panel arrastrado fuera del main window). // Usa imgui_internal.h para acceder a g.Windows y a TitleBarRect(), y dibuja // directamente en la DrawList propia de cada ventana — asi el badge va en // el mismo paso de render que el resto del panel, sin depender de // ForegroundDrawList del viewport (que no se renderiza en algunas combos de // backend + multi-viewport). // // Si tenemos icono GL cargado (Windows con appicon.ico embebido), se dibuja // el icono real (mismo bitmap que el taskbar). Sin icono, fallback a // cuadrado redondeado del color accent con la inicial blanca del app name. // // Filtro de ventanas: // - Activa, no Hidden, no Collapsed. // - No es child window. // - No es popup/tooltip/menu (NoTitleBar => skip). // - No esta dockeada en un nodo (DockIsActive => su titlebar es tabbar del // host; el host window aparece en g.Windows por separado y SI recibe badge). // - Su viewport != main viewport. static void draw_header_badge_on_floating_panels(const AppConfig& cfg) { if (!cfg.header_badge.enabled) return; ImGuiContext& g = *ImGui::GetCurrentContext(); ImGuiViewport* main_vp = ImGui::GetMainViewport(); const ImU32 bg = resolve_badge_color(cfg); const char* glyph = resolve_badge_glyph(cfg); const float sz = cfg.header_badge.size_px > 4.0f ? cfg.header_badge.size_px : 18.0f; const float mg = cfg.header_badge.margin_px >= 0.0f ? cfg.header_badge.margin_px : 6.0f; const float round = sz * 0.22f; const bool has_texture = (g_app_icon_texture != 0); for (int i = 0; i < g.Windows.Size; ++i) { ImGuiWindow* w = g.Windows[i]; if (!w || !w->WasActive || w->Hidden) continue; if (w->Flags & ImGuiWindowFlags_ChildWindow) continue; if (w->Flags & ImGuiWindowFlags_NoTitleBar) continue; if (w->DockIsActive) continue; // titlebar reemplazado por tab bar del host if (w->Viewport == nullptr || w->Viewport == main_vp) continue; if (w->Collapsed) continue; ImRect tb = w->TitleBarRect(); ImVec2 p0(tb.Min.x + mg, tb.Min.y + (tb.GetHeight() - sz) * 0.5f); ImVec2 p1(p0.x + sz, p0.y + sz); ImDrawList* dl = w->DrawList; if (has_texture) { dl->AddImageRounded((ImTextureID)(intptr_t)g_app_icon_texture, p0, p1, ImVec2(0,0), ImVec2(1,1), IM_COL32_WHITE, round); } else { dl->AddRectFilled(p0, p1, bg, round); ImVec2 ts = ImGui::CalcTextSize(glyph); ImVec2 tp(p0.x + (sz - ts.x) * 0.5f, p0.y + (sz - ts.y) * 0.5f); dl->AddText(tp, IM_COL32(255, 255, 255, 255), glyph); } } } // Resuelve si la ventana GLFW debe crearse oculta (GLFW_VISIBLE=FALSE). // default_hidden : politica base del path de entrada (apps reales = false, // tests de UI = true). // config_headless: AppConfig.headless explicito de la app. // El entorno FN_HEADLESS gana sobre ambos: "0"/"false" fuerza visible, // cualquier otro valor no vacio fuerza oculta. Sin la variable, se respeta // default_hidden || config_headless. static bool resolve_headless(bool default_hidden, bool config_headless) { bool hidden = default_hidden || config_headless; if (const char* e = std::getenv("FN_HEADLESS")) { if (std::strcmp(e, "0") == 0 || std::strcmp(e, "false") == 0) { hidden = false; } else if (e[0] != '\0') { hidden = true; } } return hidden; } int run_app(AppConfig config, std::function render_fn) { // Logger primero para capturar fallos del propio init (GLFW, ventana, GL). if (config.log.file_path != nullptr) { fn_log::logger_init( config.log.file_path, static_cast(config.log.level)); fn_log::log_info("app start: %s", config.title ? config.title : "(no title)"); } glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) { fprintf(stderr, "Failed to initialize GLFW\n"); fn_log::log_error("GLFW init failed"); if (config.log.file_path != nullptr) fn_log::logger_close(); return 1; } // OpenGL 4.3 core (issue 0049b) — habilita compute shaders, SSBOs, image // load/store, atomic counters y debug output. Backward-compatible con // shaders #version 330 y con todo lo escrito para 3.3 core. glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Apps reales: ventana visible por defecto. Solo se oculta si la app pide // headless o el entorno FN_HEADLESS lo fuerza (smoke/capture sin parpadeo). const bool hidden = resolve_headless(/*default_hidden=*/false, config.headless); glfwWindowHint(GLFW_VISIBLE, hidden ? GLFW_FALSE : GLFW_TRUE); GLFWwindow* window = glfwCreateWindow(config.width, config.height, config.title, nullptr, nullptr); if (!window) { fprintf(stderr, "Failed to create GLFW window\n"); fn_log::log_error("GLFW createWindow failed (%dx%d)", config.width, config.height); if (config.log.file_path != nullptr) fn_log::logger_close(); glfwTerminate(); return 1; } glfwMakeContextCurrent(window); glfwSwapInterval(config.vsync ? 1 : 0); // Anti-jitter: when the OS moves/resizes the window externally (Windows // tools like AltSnap, tiling WMs, snap-assist), ImGui's viewport pos can // lag one frame and `UpdatePlatformWindows` reapplies the stale value via // glfwSetWindowPos, fighting the OS and producing visible jitter. // Updating the viewport struct directly from the GLFW callback closes the // loop in the same tick — no stale Pos can ever reach the platform sync. // ImGui_ImplGlfw_InitForOpenGL does NOT install pos/size callbacks, so we // can install ours without breaking the backend's own callback chain. glfwSetWindowPosCallback(window, [](GLFWwindow* w, int x, int y) { if (ImGui::GetCurrentContext() == nullptr) return; if (ImGuiViewport* vp = ImGui::FindViewportByPlatformHandle(w)) { vp->Pos = ImVec2((float)x, (float)y); } }); glfwSetWindowSizeCallback(window, [](GLFWwindow* w, int cx, int cy) { if (ImGui::GetCurrentContext() == nullptr) return; if (ImGuiViewport* vp = ImGui::FindViewportByPlatformHandle(w)) { vp->Size = ImVec2((float)cx, (float)cy); } }); #ifdef _WIN32 // Install Win32 WndProc subclass to detect WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE. // External movers (AltSnap) fake these brackets without blocking the app // thread; we observe them and skip render+swap so the compositor moves // the existing buffer (same contract as native title-bar drag). install_sizemove_subclass(window); // Adjuntar appicon embebido al HWND principal para que aparezca en la // barra de tareas, Alt+Tab y title bar (GLFW no propaga el icono de // recursos del .exe a su WNDCLASS por defecto). attach_app_icon_to_hwnd(glfwGetWin32Window(window)); // Title bar oscuro (DWM) si el tema lo es. Sin esto el header del SO // queda blanco aunque el cliente sea dark. { const bool dark = (config.theme == ThemeMode::FnDark || config.theme == ThemeMode::ImGuiDark); attach_dark_titlebar_to_hwnd(glfwGetWin32Window(window), dark); } #endif // Carga punteros a funciones GL >= 2.0 si la app lo pide. En Linux es // no-op; en Windows usa wglGetProcAddress (requiere ctx GL activo). if (config.init_gl_loader) { if (!fn::gfx::gl_loader_init()) { fprintf(stderr, "Failed to initialize GL function loader\n"); fn_log::log_error("gl_loader_init failed"); if (config.log.file_path != nullptr) fn_log::logger_close(); glfwDestroyWindow(window); glfwTerminate(); return 1; } } // Setup ImGui IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImPlot::CreateContext(); ImPlot3D::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Multi-viewport docking: payload viewport y target viewport swappean // buffers independientes, asi que los dock preview overlays (los rects // azul/gris que indican zonas droppeables) parecen vibrar 1px contra el // payload arrastrado. Con TransparentPayload el payload se vuelve // invisible al arrastrar y los rects solo se pintan en el target -> // ningun desync visible. Recomendado por upstream cuando "rendering of // multiple viewport cannot be synced". io.ConfigDockingTransparentPayload = true; // Title-bar-only move for ImGui windows. Critical for secondary viewports // (floating panels) whose entire OS window is a single borderless ImGui // window: without this flag, ImGui moves the window when the user drags // any empty client-area pixel, which translates to the OS viewport // following the mouse "from anywhere" with no modifier. With this flag, // floating panels obey the same "header only" contract as a native // decorated window. Alt+LMB anywhere still moves via our WndProc subclass // (consumed before ImGui sees the click). io.ConfigWindowsMoveFromTitleBarOnly = true; // Convencion local_files: imgui.ini y app_settings.ini viven en // /local_files/. Migra automaticamente desde el cwd o // exe_dir si vienen de una version previa. { static const char* legacy_names[] = {"imgui.ini", "app_settings.ini"}; migrate_to_local_files(legacy_names, sizeof(legacy_names) / sizeof(legacy_names[0])); } static std::string s_imgui_ini = local_path("imgui.ini"); io.IniFilename = s_imgui_ini.c_str(); // Lee app_settings.ini (font_id, font_size_px, show_fps) antes de cargar // fuentes. Si no existe el .ini, los defaults se aplican. fn_ui::settings_load(); // Auto-wiring del menu Layouts: si la app no proporciono layouts_cb y no // ha desactivado auto_layouts, abrimos un LayoutStorage por defecto con // SQLite en `/` y generamos los callbacks // estandar (list/save/apply/delete/reset). Asi toda app C++ obtiene el // menu Layouts gratis sin codigo. fn_ui::LayoutStorage* auto_layouts_storage = nullptr; fn_ui::LayoutCallbacks auto_layouts_cb; if (config.layouts_cb == nullptr && config.auto_layouts) { const char* db_name = (config.auto_layouts_db && *config.auto_layouts_db) ? config.auto_layouts_db : "layouts.db"; auto_layouts_storage = fn_ui::layout_storage_open(local_path(db_name)); if (auto_layouts_storage) { fn_ui::layout_storage_make_callbacks(auto_layouts_storage, auto_layouts_cb); config.layouts_cb = &auto_layouts_cb; // Restore-on-open: si hay un layout activo persistido, lo dejamos // pendiente para que el primer frame del main loop lo aplique via // layout_storage_apply_pending. Asi la app abre con el ultimo // layout que el usuario tenia activo. active_name se setea ya // optimista para reflejarlo en el menu desde el primer frame. std::string last = fn_ui::layout_storage_get_last_active(auto_layouts_storage); if (!last.empty() && fn_ui::layout_storage_apply(auto_layouts_storage, last)) { auto_layouts_cb.active_name = last; fn_log::log_info("auto_layouts: restaurado layout '%s'", last.c_str()); } } else { fn_log::log_warn("auto_layouts: layout_storage_open fallo (%s)", db_name); } } // Registra info de la ventana About si la app la proveyo en AppConfig. if (config.about.name != nullptr) { fn_ui::about_window_set_info( config.about.name, config.about.version ? config.about.version : "", config.about.description ? config.about.description : ""); } // Texto vectorial (Karla / Roboto / DroidSans / Cousine, segun settings) // + iconos Tabler mergeados al mismo tamaño en el mismo ImFont. fn_ui::load_fonts_from_settings(); // ImGui 1.92+ usa style.FontSizeBase como tamaño activo (escalable sin // rebuild de atlas). Inicializa al valor del .ini para que el primer // frame ya respete el setting. { ImGuiStyle& style = ImGui::GetStyle(); style.FontSizeBase = fn_ui::settings().font_size_px; style._NextFrameFontSizeBase = style.FontSizeBase; } if (config.viewports) { io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; } // Identidad visual — ver cpp/DESIGN_SYSTEM.md switch (config.theme) { case ThemeMode::FnDark: fn_tokens::apply_dark_theme(); break; case ThemeMode::ImGuiDark: ImGui::StyleColorsDark(); break; case ThemeMode::ImGuiLight: ImGui::StyleColorsLight(); break; case ThemeMode::None: break; } // When viewports are enabled, tweak WindowRounding/WindowBg so // platform windows look consistent with the main window if (config.viewports) { ImGuiStyle& style = ImGui::GetStyle(); style.WindowRounding = 0.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f; } ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); // Main loop while (!glfwWindowShouldClose(window)) { glfwPollEvents(); // When the main window is iconified we used to glfwWaitEvents+continue // to save CPU. That is wrong when floating panels (secondary // viewports) exist: skipping the frame stops UpdatePlatformWindows / // RenderPlatformWindowsDefault so those panels go blank or get // ungrouped by the WM. We therefore detect secondary viewports first // and, if any are alive, fall through to a normal frame (main GL // clear/swap is harmless on the hidden main HWND, secondary GL // contexts keep refreshing). Only when there are NO floating panels // do we sleep on glfwWaitEvents the way we used to. if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) { bool has_secondary_viewport = false; if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ImGuiPlatformIO& pio_ic = ImGui::GetPlatformIO(); for (int i = 0; i < pio_ic.Viewports.Size; ++i) { ImGuiViewport* vp = pio_ic.Viewports[i]; if (!vp || !vp->PlatformHandle) continue; if ((GLFWwindow*)vp->PlatformHandle == window) continue; has_secondary_viewport = true; break; } } if (!has_secondary_viewport) { glfwWaitEvents(); continue; } // fallthrough: render normally so floating panels stay alive. } #ifdef _WIN32 // Subclass any platform window we haven't subclassed yet. Covers the // main window AND every secondary viewport (panels dragged outside // main) so AltSnap's WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE brackets are // observed regardless of which HWND it targets. Runs BEFORE the // sizemove gate below so newly-created secondaries are protected from // their very first frame onwards. if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { prune_dead_subclassed(); prune_dead_icon_attached(); prune_dead_dark_titlebar(); const bool dark_tb = (config.theme == ThemeMode::FnDark || config.theme == ThemeMode::ImGuiDark); ImGuiPlatformIO& pio_sub = ImGui::GetPlatformIO(); for (int i = 0; i < pio_sub.Viewports.Size; ++i) { ImGuiViewport* vp = pio_sub.Viewports[i]; if (!vp || !vp->PlatformHandle) continue; GLFWwindow* gw = (GLFWwindow*)vp->PlatformHandle; install_sizemove_subclass(gw); // Floating panels = secondary HWNDs creados por el backend // GLFW. WNDCLASS distinta de la main -> no heredan icono via // SetClassLongPtrW. WM_SETICON per-HWND es la unica forma de // que el taskbar/titlebar muestren el icono. attach_app_icon_to_hwnd(glfwGetWin32Window(gw)); // Misma logica para el title bar oscuro — cada viewport // secundario tiene su propio HWND con caption pintado por DWM. attach_dark_titlebar_to_hwnd(glfwGetWin32Window(gw), dark_tb); } } #endif // While an external mover (AltSnap on Win32, tiling WMs) is dragging // the window we mirror the native title-bar contract: do not render, // do not swap, just pump events. The DWM compositor scrolls the last // presented framebuffer with the window — no race between SetWindowPos // (async) and glfwSwapBuffers, so no jitter. WM_EXITSIZEMOVE clears // the flag and the main loop resumes normal rendering. Applies to // brackets on ANY subclassed HWND (main or secondary viewports). if (external_sizemove_active()) { // Bound the busy loop so the message queue gets drained but we // don't burn CPU when AltSnap pauses between mouse moves. glfwWaitEventsTimeout(0.016); continue; } // Anti-jitter pass 2: covers secondary viewport windows that the // backend creates dynamically (panels dragged outside the main). // Sync each viewport's Pos/Size to the OS-reported state BEFORE // NewFrame, so ImGui logic this tick already sees the up-to-date // values and UpdatePlatformWindows can't stomp them with stale data. if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ImGuiPlatformIO& pio = ImGui::GetPlatformIO(); for (int i = 0; i < pio.Viewports.Size; ++i) { ImGuiViewport* vp = pio.Viewports[i]; if (!vp || !vp->PlatformHandle) continue; GLFWwindow* gw = (GLFWwindow*)vp->PlatformHandle; int x = 0, y = 0, cx = 0, cy = 0; glfwGetWindowPos(gw, &x, &y); glfwGetWindowSize(gw, &cx, &cy); vp->Pos = ImVec2((float)x, (float)y); vp->Size = ImVec2((float)cx, (float)cy); } } // Tamaño de fuente: aplica via style.FontSizeBase cada frame. Cambios // se ven al instante (ImGui 1.92+ escala el atlas dinamicamente, no // hace falta rebuild). ImGuiStyle& style = ImGui::GetStyle(); if (style.FontSizeBase != fn_ui::settings().font_size_px) { style.FontSizeBase = fn_ui::settings().font_size_px; style._NextFrameFontSizeBase = style.FontSizeBase; // FIXME-ImGui hack } // Cambio de fuente (font_id): rebuild atlas. ImGui_ImplOpenGL3 // refresca la GPU texture via UpdateTexture en RenderDrawData. if (fn_ui::settings_consume_font_dirty()) { fn_ui::load_fonts_from_settings(); } ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // Si auto_layouts esta gestionando el storage, aplica el layout // pendiente ANTES de que el render_fn cree ventanas. Si la app gestiona // su propio storage, debe usar cfg.pre_frame para llamar // layout_storage_apply_pending en el mismo punto. if (auto_layouts_storage) { std::string applied = fn_ui::layout_storage_apply_pending(auto_layouts_storage); if (!applied.empty()) auto_layouts_cb.active_name = applied; } // Hook pre-frame de la app — se ejecuta despues de NewFrame y antes // de menubar/auto-dockspace. Punto correcto para LoadIniSettingsFromMemory. if (config.pre_frame) { config.pre_frame(); } // Menubar canonica (View / Layouts / Settings / About) — siempre se // renderiza para que Settings/Logs/About esten disponibles aunque la // app no declare panels/layouts/view_extras propios. Se dibuja ANTES // del render_fn para que pueda hacer DockSpaceOverViewport debajo. { // Adapter: std::function -> ViewMenuExtrasFn(void*). fn_ui::ViewMenuExtrasFn extras_fn = nullptr; void* extras_user = nullptr; if ((bool)config.view_extras) { extras_fn = [](void* ud) -> bool { auto* fn_ptr = static_cast*>(ud); return (*fn_ptr) ? (*fn_ptr)() : false; }; extras_user = (void*)&config.view_extras; } fn_ui::app_menubar(config.panels, config.panel_count, config.layouts_cb, extras_fn, extras_user); } // Auto-dockspace central. Permite re-anclar ventanas flotantes al // main viewport sin que cada app llame DockSpaceOverViewport en su // render(). Apps con layout custom ponen cfg.auto_dockspace=false. if (config.auto_dockspace) { ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); } render_fn(); // Ventana de Settings (no-op si esta cerrada). fn_ui::settings_window_render(); // Ventana de Logs (no-op si esta cerrada). fn_ui::log_window_render(); // Ventana About (no-op si esta cerrada). fn_ui::about_window_render(); // FPS overlay si esta activado en Settings. if (fn_ui::settings().show_fps) { fps_overlay(); } // Identidad por app en viewports secundarios — badge en el title bar // de cada panel arrastrado fuera del main window. Si Windows + tiene // appicon.ico embebido, dibuja el mismo icono que el taskbar (PNG // RGBA escalado). Si no, fallback a cuadrado accent + inicial. #ifdef _WIN32 if (g_app_icon_texture == 0) { g_app_icon_texture = upload_hicon_to_gl_texture(); } #endif draw_header_badge_on_floating_panels(config); ImGui::Render(); int display_w, display_h; glfwGetFramebufferSize(window, &display_w, &display_h); glViewport(0, 0, display_w, display_h); glClearColor(config.bg_r, config.bg_g, config.bg_b, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); // Multi-viewport: update and render platform windows if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { GLFWwindow* backup_ctx = glfwGetCurrentContext(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); glfwMakeContextCurrent(backup_ctx); } glfwSwapBuffers(window); #ifdef TRACY_ENABLE FrameMark; #endif } // Persiste settings al exit (idempotente con auto-saves del menu). fn_ui::settings_save(); // Cierra el archivo de log (si la app lo abrio). if (config.log.file_path != nullptr) { fn_log::log_info("app exit"); fn_log::logger_close(); } // Save-on-close: si hay un layout activo, persiste el INI actual en su // slot para que la proxima apertura cargue exactamente el mismo estado // (incluye los retoques de docking/posiciones que el usuario hizo // durante la sesion). Tambien reescribe last_active por si el callback // se salto. Hecho ANTES de cerrar el storage. Necesita ImGui context // vivo (SaveIniSettingsToMemory), por eso va antes de DestroyContext. if (auto_layouts_storage && !auto_layouts_cb.active_name.empty()) { fn_ui::layout_storage_save(auto_layouts_storage, auto_layouts_cb.active_name); fn_ui::layout_storage_set_last_active(auto_layouts_storage, auto_layouts_cb.active_name); } // Cierra el storage de layouts auto-creado, si lo hay. if (auto_layouts_storage) { fn_ui::layout_storage_close(auto_layouts_storage); auto_layouts_storage = nullptr; } // Cleanup if (g_app_icon_texture != 0) { glDeleteTextures(1, &g_app_icon_texture); g_app_icon_texture = 0; } ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImPlot3D::DestroyContext(); ImPlot::DestroyContext(); ImGui::DestroyContext(); #ifdef _WIN32 uninstall_sizemove_subclass_all(); #endif glfwDestroyWindow(window); glfwTerminate(); return 0; } int run_app(std::function render_fn) { return run_app(AppConfig{}, render_fn); } // Test-only observability of the Win32 subclass. Always defined (zero cost); // on non-Windows the counters never increment. namespace internal { int sizemove_enter_count() { #ifdef _WIN32 return g_sizemove_enter_count.load(std::memory_order_acquire); #else return 0; #endif } int alt_rmb_resize_count() { #ifdef _WIN32 return g_alt_rmb_resize_count.load(std::memory_order_acquire); #else return 0; #endif } void set_force_alt_for_test(bool v) { #ifdef _WIN32 g_force_alt_for_test.store(v, std::memory_order_release); #else (void)v; #endif } int rbuttondown_seen_count() { #ifdef _WIN32 return g_rbuttondown_seen_count.load(std::memory_order_acquire); #else return 0; #endif } int alt_lmb_move_count() { #ifdef _WIN32 return g_alt_lmb_move_count.load(std::memory_order_acquire); #else return 0; #endif } } // namespace internal } // namespace fn #ifdef IMGUI_ENABLE_TEST_ENGINE #include "imgui_te_engine.h" #include "imgui_te_ui.h" #include "imgui_te_context.h" #include "imgui_te_exporters.h" namespace fn { int run_app_test(AppConfig config, std::function render_fn, std::function register_tests, const char* filter) { if (!register_tests) { fprintf(stderr, "run_app_test: register_tests callback is null\n"); return 1; } glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) { fprintf(stderr, "GLFW init failed\n"); return 1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Tests de frontend: ventana OCULTA por defecto (headless) para no parpadear // en la pantalla del desarrollador ni robar foco mientras el Test Engine // ejercita la UI. El contexto GL real se crea igual, asi que el render sigue // siendo fiel. Opt-out para depurar visualmente: FN_HEADLESS=0. const bool hidden = resolve_headless(/*default_hidden=*/true, config.headless); glfwWindowHint(GLFW_VISIBLE, hidden ? GLFW_FALSE : GLFW_TRUE); GLFWwindow* window = glfwCreateWindow( config.width, config.height, config.title ? config.title : "fn_test", nullptr, nullptr); if (!window) { glfwTerminate(); fprintf(stderr, "createWindow failed\n"); return 1; } glfwMakeContextCurrent(window); glfwSwapInterval(0); // tests run as fast as possible — no vsync if (config.init_gl_loader) { if (!fn::gfx::gl_loader_init()) { glfwDestroyWindow(window); glfwTerminate(); fprintf(stderr, "gl_loader_init failed\n"); return 1; } } IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImPlot::CreateContext(); ImPlot3D::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // No viewports in tests — the engine drives the main window only. io.IniFilename = nullptr; // tests don't persist .ini fn_ui::settings_load(); fn_ui::load_fonts_from_settings(); switch (config.theme) { case ThemeMode::FnDark: fn_tokens::apply_dark_theme(); break; case ThemeMode::ImGuiDark: ImGui::StyleColorsDark(); break; case ThemeMode::ImGuiLight: ImGui::StyleColorsLight(); break; case ThemeMode::None: break; } ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); // --- Test engine setup --- ImGuiTestEngine* engine = ImGuiTestEngine_CreateContext(); ImGuiTestEngineIO& te_io = ImGuiTestEngine_GetIO(engine); te_io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info; te_io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug; te_io.ConfigRunSpeed = ImGuiTestRunSpeed_Fast; te_io.ConfigStopOnError = false; te_io.ConfigCaptureEnabled = false; te_io.ConfigSavedSettings = false; register_tests(engine); ImGuiTestEngine_Start(engine, ImGui::GetCurrentContext()); ImGuiTestEngine_QueueTests(engine, ImGuiTestGroup_Tests, filter, ImGuiTestRunFlags_RunFromCommandLine); // --- Loop until tests finish --- bool tests_queued_done = false; int frames_after_done = 0; while (!glfwWindowShouldClose(window)) { glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); if (config.pre_frame) config.pre_frame(); render_fn(); ImGui::Render(); int display_w, display_h; glfwGetFramebufferSize(window, &display_w, &display_h); glViewport(0, 0, display_w, display_h); glClearColor(config.bg_r, config.bg_g, config.bg_b, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); if (!tests_queued_done && ImGuiTestEngine_IsTestQueueEmpty(engine)) { tests_queued_done = true; } if (tests_queued_done) { // Let the engine flush its final state for a few frames before exit. if (++frames_after_done > 2) break; } } int count_tested = 0, count_success = 0; ImGuiTestEngine_GetResult(engine, count_tested, count_success); bool all_passed = (count_tested > 0) && (count_tested == count_success); ImGuiTestEngine_PrintResultSummary(engine); ImGuiTestEngine_Stop(engine); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImPlot3D::DestroyContext(); ImPlot::DestroyContext(); ImGui::DestroyContext(); ImGuiTestEngine_DestroyContext(engine); glfwDestroyWindow(window); glfwTerminate(); fprintf(stdout, "\n[fn::run_app_test] %d/%d tests passed%s\n", count_success, count_tested, all_passed ? "" : " — FAILED"); return all_passed ? 0 : 1; } } // namespace fn #endif // IMGUI_ENABLE_TEST_ENGINE