feat(cpp/core): logger + log_window + selectable_text widgets

Logger global thread-safe con ring buffer in-memory de 2000 entradas + escritura
opcional a archivo. log_window flotante consume el ring buffer con filtros por
nivel, busqueda y autoscroll; se abre desde Settings -> Logs en la menubar.
selectable_text cubre el patron drag-to-select + Ctrl+C en cualquier ventana.

app_menubar y framework run_app integran log_window_render() en el frame loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 11:50:57 +02:00
parent d55b8fea5d
commit f1a5e04d4f
17 changed files with 1280 additions and 64 deletions
+150 -2
View File
@@ -316,8 +316,20 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
// 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);
if (config.panels != nullptr || config.layouts_cb != nullptr ||
(bool)config.view_extras) {
// Adapter: std::function<bool()> -> 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<std::function<bool()>*>(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);
}
render_fn();
@@ -385,3 +397,139 @@ int run_app(std::function<void()> render_fn) {
}
} // 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<void()> render_fn,
std::function<void(::ImGuiTestEngine*)> 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);
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();
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