#include "app_base.h" #include "imgui.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 "gfx/gl_loader.h" #include #include #include #include #include #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #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); } 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(); } 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; } } } 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); 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); // 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; // 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(); // 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(); if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) { glfwWaitEvents(); continue; } // 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(); // Menubar canonica (View / Layouts / Settings / About) si la app la // configuro en AppConfig. Se renderiza ANTES del render_fn para que // el render_fn pueda hacer DockSpaceOverViewport debajo. if (config.panels != nullptr || config.layouts_cb != nullptr) { fn_ui::app_menubar(config.panels, config.panel_count, config.layouts_cb); } 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(); } 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(); } // Cleanup ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImPlot3D::DestroyContext(); ImPlot::DestroyContext(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); return 0; } int run_app(std::function render_fn) { return run_app(AppConfig{}, render_fn); } } // namespace fn