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 // Menubar canonica (View / Layouts / Settings / About) si la app la
// configuro en AppConfig. Se renderiza ANTES del render_fn para que // configuro en AppConfig. Se renderiza ANTES del render_fn para que
// el render_fn pueda hacer DockSpaceOverViewport debajo. // el render_fn pueda hacer DockSpaceOverViewport debajo.
if (config.panels != nullptr || config.layouts_cb != nullptr) { if (config.panels != nullptr || config.layouts_cb != nullptr ||
fn_ui::app_menubar(config.panels, config.panel_count, config.layouts_cb); (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(); render_fn();
@@ -385,3 +397,139 @@ int run_app(std::function<void()> render_fn) {
} }
} // namespace 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
+39
View File
@@ -115,6 +115,13 @@ struct AppConfig {
// llama fn_ui::app_menubar(panels, panel_count, layouts_cb) cada frame. // llama fn_ui::app_menubar(panels, panel_count, layouts_cb) cada frame.
fn_ui::LayoutCallbacks* layouts_cb = nullptr; fn_ui::LayoutCallbacks* layouts_cb = nullptr;
// Items extra dentro del menu "View", al final tras los toggles de
// paneles. Si view_extras != nullptr, run_app lo pasa a app_menubar.
// El callback se invoca dentro de un BeginMenu("View") ya abierto:
// la app llama directamente a ImGui::Separator(), MenuItem(), etc.
// NO debe abrir/cerrar el menu View.
std::function<bool()> view_extras{};
// Si true, run_app llama fn::gfx::gl_loader_init() tras crear el contexto // Si true, run_app llama fn::gfx::gl_loader_init() tras crear el contexto
// GL y antes del primer frame. Necesario para apps que llaman gl* directo // GL y antes del primer frame. Necesario para apps que llaman gl* directo
// en Windows (en Linux es no-op). // en Windows (en Linux es no-op).
@@ -136,3 +143,35 @@ int run_app(AppConfig config, std::function<void()> render_fn);
int run_app(std::function<void()> render_fn); int run_app(std::function<void()> render_fn);
} // namespace fn } // namespace fn
// ----------------------------------------------------------------------------
// E2E testing — Dear ImGui Test Engine integration.
// ----------------------------------------------------------------------------
//
// Only available when the registry is built with -DFN_BUILD_TESTS=ON. The
// CMake option defines IMGUI_ENABLE_TEST_ENGINE on imgui+fn_framework and
// links the imgui_test_engine static lib. Without the option, run_app_test is
// not declared and apps build identically to today.
#ifdef IMGUI_ENABLE_TEST_ENGINE
struct ImGuiTestEngine;
namespace fn {
// Run an app under Dear ImGui Test Engine. Same as run_app, but:
// 1. Creates a test engine and binds it to the ImGui context.
// 2. Calls register_tests(engine) once before the main loop. Tests are
// registered with IM_REGISTER_TEST(engine, "category", "name") and
// assigned a TestFunc lambda that drives the UI.
// 3. Queues all tests matching `filter` (default "all") and ticks frames
// until the queue empties.
// 4. Exits with code 0 if all tests pass, 1 if any failed or crashed.
//
// register_tests must be non-null and must register at least one test or the
// function returns 1.
int run_app_test(AppConfig config,
std::function<void()> render_fn,
std::function<void(ImGuiTestEngine*)> register_tests,
const char* filter = "all");
} // namespace fn
#endif // IMGUI_ENABLE_TEST_ENGINE
+27 -5
View File
@@ -1,17 +1,38 @@
#include "core/app_menubar.h" #include "core/app_menubar.h"
#include "core/log_window.h"
#include <imgui.h> #include <imgui.h>
namespace fn_ui { namespace fn_ui {
bool app_menubar(const PanelToggle* panels, std::size_t count, bool app_menubar(const PanelToggle* panels, std::size_t count,
LayoutCallbacks* layouts_cb) { LayoutCallbacks* layouts_cb,
ViewMenuExtrasFn view_extras,
void* view_extras_user) {
if (!ImGui::BeginMainMenuBar()) return false; if (!ImGui::BeginMainMenuBar()) return false;
bool changed = false; bool changed = false;
// Menu "View" — solo si hay panels // Menu "View" — combinamos los toggles de paneles con los extras de
if (panels && count > 0) { // la app (si los hay) bajo el mismo BeginMenu para que el usuario los
changed |= panel_menu_items("View", panels, count); // vea juntos. Si no hay panels NI extras, omitimos el menu entero.
bool has_panels = (panels && count > 0);
if (has_panels || view_extras) {
if (ImGui::BeginMenu("View")) {
if (has_panels) {
for (std::size_t i = 0; i < count; ++i) {
const PanelToggle& item = panels[i];
if (!item.open) continue;
if (ImGui::MenuItem(item.label, item.shortcut, item.open)) {
changed = true;
}
}
}
if (view_extras) {
if (has_panels) ImGui::Separator();
changed |= view_extras(view_extras_user);
}
ImGui::EndMenu();
}
} }
// Menu "Layouts" — solo si hay callbacks // Menu "Layouts" — solo si hay callbacks
@@ -19,10 +40,11 @@ bool app_menubar(const PanelToggle* panels, std::size_t count,
changed |= layouts_menu_items("Layouts", *layouts_cb); changed |= layouts_menu_items("Layouts", *layouts_cb);
} }
// Menu "Settings" — siempre. Submenus: Settings... y About... // Menu "Settings" — siempre. Submenus: Settings... / Logs... / About...
// Las ventanas se renderizan al final del frame en fn::run_app. // Las ventanas se renderizan al final del frame en fn::run_app.
if (ImGui::BeginMenu("Settings")) { if (ImGui::BeginMenu("Settings")) {
changed |= settings_window_menu_item("Settings..."); changed |= settings_window_menu_item("Settings...");
changed |= log_window_menu_item("Logs...");
ImGui::Separator(); ImGui::Separator();
changed |= about_window_menu_item("About..."); changed |= about_window_menu_item("About...");
ImGui::EndMenu(); ImGui::EndMenu();
+13 -2
View File
@@ -7,6 +7,14 @@
namespace fn_ui { namespace fn_ui {
// Callback opcional que la app puede inyectar para añadir items propios
// al final del menu "View", justo despues de los toggles de paneles.
// El callback se invoca dentro de un `BeginMenu("View")` ya abierto: la
// app llama directamente a `ImGui::Separator()`, `ImGui::MenuItem(...)`,
// `ImGui::BeginDisabled()`, etc. NO debe hacer Begin/EndMenu para "View".
// Devuelve true si el usuario activo algun item este frame.
using ViewMenuExtrasFn = bool(*)(void* user_data);
// Renderiza una MainMenuBar completa con: // Renderiza una MainMenuBar completa con:
// * Menu "View" (panel_menu_items con los toggles dados) [si panels] // * Menu "View" (panel_menu_items con los toggles dados) [si panels]
// * Menu "Layouts" (layouts_menu_items con las callbacks dadas) [si layouts_cb] // * Menu "Layouts" (layouts_menu_items con las callbacks dadas) [si layouts_cb]
@@ -16,13 +24,16 @@ namespace fn_ui {
// //
// Llamar despues de NewFrame() y antes del DockSpaceOverViewport. // Llamar despues de NewFrame() y antes del DockSpaceOverViewport.
// Si layouts_cb es nullptr, omite Layouts. // Si layouts_cb es nullptr, omite Layouts.
// Si panels es nullptr o count == 0, omite View. // Si panels es nullptr o count == 0, omite View (o solo dibuja extras
// si view_extras != nullptr).
// Las ventanas Settings y About se renderizan al final del frame en // Las ventanas Settings y About se renderizan al final del frame en
// fn::run_app via settings_window_render() y about_window_render(). // fn::run_app via settings_window_render() y about_window_render().
// //
// Returns: true si el usuario togglo paneles, disparo accion de layouts, // Returns: true si el usuario togglo paneles, disparo accion de layouts,
// o abrio una ventana este frame. // o abrio una ventana este frame.
bool app_menubar(const PanelToggle* panels, std::size_t count, bool app_menubar(const PanelToggle* panels, std::size_t count,
LayoutCallbacks* layouts_cb); LayoutCallbacks* layouts_cb,
ViewMenuExtrasFn view_extras = nullptr,
void* view_extras_user = nullptr);
} // namespace fn_ui } // namespace fn_ui
+9 -1
View File
@@ -8,7 +8,7 @@ purity: pure
signature: "bool fn_ui::app_menubar(const fn_ui::PanelToggle* panels, size_t count, fn_ui::LayoutCallbacks* layouts_cb)" signature: "bool fn_ui::app_menubar(const fn_ui::PanelToggle* panels, size_t count, fn_ui::LayoutCallbacks* layouts_cb)"
description: "MainMenuBar ImGui completa con menu View (toggles de paneles) y menu Layouts (guardar/aplicar layouts persistentes). Punto de entrada unificado para la menubar de cualquier app fn_ui." description: "MainMenuBar ImGui completa con menu View (toggles de paneles) y menu Layouts (guardar/aplicar layouts persistentes). Punto de entrada unificado para la menubar de cualquier app fn_ui."
tags: [imgui, ui, menu, panels, layouts, dockspace, menubar] tags: [imgui, ui, menu, panels, layouts, dockspace, menubar]
uses_functions: ["app_about_cpp_core", "app_settings_cpp_core", "layouts_menu_cpp_core", "panel_menu_cpp_core"] uses_functions: ["app_about_cpp_core", "app_settings_cpp_core", "layouts_menu_cpp_core", "panel_menu_cpp_core", "log_window_cpp_core"]
uses_types: [] uses_types: []
returns: [] returns: []
returns_optional: false returns_optional: false
@@ -135,3 +135,11 @@ El item plano `Settings...` pasa a ser un `BeginMenu("Settings")` con dos subite
`fn::run_app` ahora llama tambien `fn_ui::about_window_render()` al final del frame, justo despues de `settings_window_render()`. Apps que no usan `fn::run_app` deben llamar ambos manualmente. `fn::run_app` ahora llama tambien `fn_ui::about_window_render()` al final del frame, justo despues de `settings_window_render()`. Apps que no usan `fn::run_app` deben llamar ambos manualmente.
Cambio retro-compatible: las apps que solo invocan `fn_ui::app_menubar(nullptr, 0, nullptr)` no necesitan tocar nada — el menu `Settings` aparece con About vacio (defaults `"fn_registry app"` / sin version) hasta que llamen `about_window_set_info`. Cambio retro-compatible: las apps que solo invocan `fn_ui::app_menubar(nullptr, 0, nullptr)` no necesitan tocar nada — el menu `Settings` aparece con About vacio (defaults `"fn_registry app"` / sin version) hasta que llamen `about_window_set_info`.
## Notas — Logs item (sesion 2026-05-01)
El submenu `Settings` añade un tercer item `Logs...` entre `Settings...` y `About...` que abre la ventana de `log_window_cpp_core`. La ventana muestra el ring buffer in-memory de `logger_cpp_core` con filtros por nivel + busqueda + autoscroll.
Funciona aunque la app no haya activado el logger en disco: la ventana siempre tiene el buffer in-memory disponible. Para activar tambien la persistencia a archivo, la app declara `AppLogConfig` en `AppConfig.log` (ver `logger_cpp_core`).
`fn::run_app` invoca `fn_ui::log_window_render()` al final del frame, justo despues de `settings_window_render()` y antes de `about_window_render()`.
+67 -54
View File
@@ -6,45 +6,86 @@ namespace fn_ui {
bool layouts_menu_items(const char* menu_label, LayoutCallbacks& cb) { bool layouts_menu_items(const char* menu_label, LayoutCallbacks& cb) {
bool acted = false; bool acted = false;
// Flag para abrir el popup al frame siguiente — NO podemos hacer
// BeginPopup dentro de BeginMenu porque MenuItem cierra el menu y
// BeginPopup queda fuera del flujo. Hay que hacer BeginPopup en el
// scope de la menubar (al nivel donde se llama esta funcion).
static bool s_open_save_popup = false;
static char s_name_buf[64] = "";
if (!ImGui::BeginMenu(menu_label)) return false; if (ImGui::BeginMenu(menu_label)) {
// ── Lista de layouts guardados ───────────────────────────────────
// ── Lista de layouts guardados ──────────────────────────────────────── if (cb.list) {
if (cb.list) { std::vector<std::string> names = cb.list();
std::vector<std::string> names = cb.list(); for (const std::string& name : names) {
for (const std::string& name : names) { std::string label;
// Construir label con marker si es el activo if (!cb.active_name.empty() && name == cb.active_name) {
std::string label; label = "* " + name;
if (!cb.active_name.empty() && name == cb.active_name) { } else {
label = "* " + name; label = " " + name;
} else { }
label = " " + name; if (ImGui::MenuItem(label.c_str()) && cb.on_apply) {
} cb.on_apply(name);
if (ImGui::MenuItem(label.c_str()) && cb.on_apply) { acted = true;
cb.on_apply(name); }
acted = true;
} }
} }
ImGui::Separator();
// ── Save current as... ──── solo levanta la flag, el popup va fuera
if (ImGui::MenuItem("Save current as...")) {
s_open_save_popup = true;
}
// ── Delete submenu ───────────────────────────────────────────────
if (ImGui::BeginMenu("Delete")) {
std::vector<std::string> names;
if (cb.list) names = cb.list();
if (names.empty()) {
ImGui::MenuItem("(no layouts)", nullptr, false, false);
} else {
for (const std::string& name : names) {
if (ImGui::MenuItem(name.c_str()) && cb.on_delete) {
cb.on_delete(name);
acted = true;
}
}
}
ImGui::EndMenu();
}
ImGui::Separator();
// ── Reset to default ─────────────────────────────────────────────
if (ImGui::MenuItem("Reset to default") && cb.on_reset) {
cb.on_reset();
acted = true;
}
ImGui::EndMenu();
} }
ImGui::Separator(); // ── Popup "Save current as..." (FUERA del BeginMenu) ──────────────────
// OpenPopup debe llamarse desde el mismo nivel donde se llama BeginPopup.
// ── Save current as... ──────────────────────────────────────────────── // Por eso disparamos s_open_save_popup arriba y aqui hacemos el OpenPopup
if (ImGui::MenuItem("Save current as...")) { // + BeginPopup en el scope de menubar.
if (s_open_save_popup) {
ImGui::OpenPopup("##save_layout"); ImGui::OpenPopup("##save_layout");
s_open_save_popup = false;
} }
// Popup "Save as..."
// Estado local del input: buffer estatico de 64 char.
static char s_name_buf[64] = "";
if (ImGui::BeginPopup("##save_layout")) { if (ImGui::BeginPopup("##save_layout")) {
ImGui::Text("Layout name:"); ImGui::Text("Layout name:");
ImGui::SetNextItemWidth(200.0f); ImGui::SetNextItemWidth(200.0f);
ImGui::InputText("##layout_name", s_name_buf, sizeof(s_name_buf)); bool enter_pressed = ImGui::InputText(
"##layout_name", s_name_buf, sizeof(s_name_buf),
ImGuiInputTextFlags_EnterReturnsTrue);
bool name_valid = s_name_buf[0] != '\0'; bool name_valid = s_name_buf[0] != '\0';
if (!name_valid) ImGui::BeginDisabled(); if (!name_valid) ImGui::BeginDisabled();
if (ImGui::Button("Save") && name_valid && cb.on_save) { if ((ImGui::Button("Save") || enter_pressed)
&& name_valid && cb.on_save) {
cb.on_save(std::string(s_name_buf)); cb.on_save(std::string(s_name_buf));
s_name_buf[0] = '\0'; s_name_buf[0] = '\0';
acted = true; acted = true;
@@ -59,34 +100,6 @@ bool layouts_menu_items(const char* menu_label, LayoutCallbacks& cb) {
} }
ImGui::EndPopup(); ImGui::EndPopup();
} }
// ── Delete submenu ────────────────────────────────────────────────────
if (ImGui::BeginMenu("Delete")) {
std::vector<std::string> names;
if (cb.list) names = cb.list();
if (names.empty()) {
ImGui::MenuItem("(no layouts)", nullptr, false, false);
} else {
for (const std::string& name : names) {
if (ImGui::MenuItem(name.c_str()) && cb.on_delete) {
cb.on_delete(name);
acted = true;
}
}
}
ImGui::EndMenu();
}
ImGui::Separator();
// ── Reset to default ──────────────────────────────────────────────────
if (ImGui::MenuItem("Reset to default") && cb.on_reset) {
cb.on_reset();
acted = true;
}
ImGui::EndMenu();
return acted; return acted;
} }
+147
View File
@@ -0,0 +1,147 @@
#include "core/log_window.h"
#include "core/logger.h"
#include "imgui.h"
#include <cstring>
namespace fn_ui {
namespace {
bool g_open = false;
bool g_show_debug = true;
bool g_show_info = true;
bool g_show_warn = true;
bool g_show_error = true;
bool g_autoscroll = true;
char g_filter_buf[128] = "";
// Tamano previo del buffer — usado para detectar entradas nuevas y forzar
// scroll-to-bottom solo cuando llegan logs (no en cada frame).
std::size_t g_prev_buffer_size = 0;
ImVec4 color_for_level(fn_log::Level level) {
switch (level) {
case fn_log::Level::Debug: return ImVec4(0.55f, 0.60f, 0.66f, 1.0f); // gris azulado
case fn_log::Level::Info: return ImVec4(0.78f, 0.82f, 0.86f, 1.0f); // texto
case fn_log::Level::Warn: return ImVec4(0.95f, 0.74f, 0.31f, 1.0f); // ambar
case fn_log::Level::Error: return ImVec4(0.95f, 0.43f, 0.43f, 1.0f); // rojo
}
return ImVec4(1, 1, 1, 1);
}
bool level_visible(fn_log::Level level) {
switch (level) {
case fn_log::Level::Debug: return g_show_debug;
case fn_log::Level::Info: return g_show_info;
case fn_log::Level::Warn: return g_show_warn;
case fn_log::Level::Error: return g_show_error;
}
return true;
}
bool matches_filter(const char* text) {
if (g_filter_buf[0] == '\0') return true;
return std::strstr(text, g_filter_buf) != nullptr;
}
} // namespace
bool log_window_is_open() { return g_open; }
void log_window_set_open(bool v) { g_open = v; }
void log_window_toggle() { g_open = !g_open; }
bool log_window_menu_item(const char* label) {
if (ImGui::MenuItem(label)) {
g_open = true;
return true;
}
return false;
}
void log_window_render() {
if (!g_open) return;
ImGui::SetNextWindowSize(ImVec2(720, 420), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Logs", &g_open, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
return;
}
// --- Toolbar: filtros por nivel + busqueda + acciones ---
ImGui::Checkbox("Debug", &g_show_debug); ImGui::SameLine();
ImGui::Checkbox("Info", &g_show_info); ImGui::SameLine();
ImGui::Checkbox("Warn", &g_show_warn); ImGui::SameLine();
ImGui::Checkbox("Error", &g_show_error);
ImGui::SameLine();
ImGui::Dummy(ImVec2(12, 0));
ImGui::SameLine();
ImGui::SetNextItemWidth(220.0f);
ImGui::InputTextWithHint("##log_filter", "filtro (substring)", g_filter_buf, sizeof(g_filter_buf));
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &g_autoscroll);
ImGui::SameLine();
if (ImGui::Button("Clear")) {
fn_log::buffer_clear();
g_prev_buffer_size = 0;
}
// Path del archivo activo (si hay)
const char* path = fn_log::logger_path();
if (path && *path) {
ImGui::SameLine();
ImGui::TextDisabled(" → %s", path);
}
ImGui::Separator();
// --- Region scroll con todas las entradas filtradas ---
const float footer_h = ImGui::GetFrameHeightWithSpacing();
if (ImGui::BeginChild("##log_scroll",
ImVec2(0, -footer_h),
true,
ImGuiWindowFlags_HorizontalScrollbar)) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1));
std::size_t total = fn_log::buffer_size();
std::size_t shown = 0;
for (std::size_t i = 0; i < total; ++i) {
const fn_log::Entry* e = fn_log::buffer_at(i);
if (!e) continue;
if (!level_visible(e->level)) continue;
if (!matches_filter(e->text)) continue;
ImVec4 col = color_for_level(e->level);
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(e->text);
ImGui::PopStyleColor();
++shown;
}
ImGui::PopStyleVar();
// Auto-scroll: solo cuando llegan entradas nuevas
if (g_autoscroll && total != g_prev_buffer_size) {
ImGui::SetScrollHereY(1.0f);
}
g_prev_buffer_size = total;
// Footer dentro del child para que se vea pegado abajo si esta vacio
if (shown == 0) {
ImGui::TextDisabled("Sin entradas que coincidan con los filtros.");
}
}
ImGui::EndChild();
// --- Status footer ---
ImGui::TextDisabled("%zu entradas en buffer (cap %zu)",
fn_log::buffer_size(), fn_log::kBufferCapacity);
ImGui::End();
}
} // namespace fn_ui
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// Ventana flotante "Logs" para apps del registry. Muestra las entradas del
// buffer in-memory de fn_log (ver logger.h) con filtros por nivel y un
// buscador por substring.
//
// Lifecycle:
// - app_menubar() incluye el MenuItem "Logs..." en el submenu "Settings"
// - end of frame: log_window_render() — invocado por fn::run_app
//
// Apps que NO usan fn::run_app deben llamar log_window_render() manualmente.
//
// El visualizador no toca el archivo en disco; solo lee el ring buffer. Si
// la app nunca llamo logger_init, la ventana sigue funcionando contra el
// buffer (los logs simplemente no se persisten).
namespace fn_ui {
bool log_window_is_open();
void log_window_set_open(bool v);
void log_window_toggle();
// MenuItem componible. Llamar dentro de un BeginMenu/BeginMainMenuBar
// exitoso. Click → abre la ventana. Returns true si el usuario clico.
bool log_window_menu_item(const char* label = "Logs...");
// Render de la ventana. No-op si is_open == false. Llamada por fn::run_app
// al final del frame, despues del render_fn de la app.
void log_window_render();
} // namespace fn_ui
+51
View File
@@ -0,0 +1,51 @@
---
name: log_window
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "bool fn_ui::log_window_is_open(); void fn_ui::log_window_set_open(bool); void fn_ui::log_window_toggle(); bool fn_ui::log_window_menu_item(const char* label = \"Logs...\"); void fn_ui::log_window_render()"
description: "Ventana flotante 'Logs' para apps C++ del registry. Muestra el ring buffer in-memory de fn_log con filtros por nivel (Debug/Info/Warn/Error), buscador por substring y autoscroll. Se abre desde el submenu Settings -> Logs... de la MainMenuBar."
tags: [imgui, logger, viewer, window, ui]
uses_functions: [logger_cpp_core]
uses_types: [log_level_cpp_core, log_entry_cpp_core]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui, cstring]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/log_window.cpp"
framework: imgui
params:
- name: v
desc: "Estado deseado de la ventana (open=true / closed=false) en log_window_set_open"
- name: label
desc: "Texto del MenuItem en la menubar (default 'Logs...')"
output: "log_window_render no-op si is_open == false. log_window_menu_item retorna true si el usuario clico el item. Las demas mutan estado global del modulo"
notes: "consumido por cpp/framework/app_base.cpp (render automatico) y cpp/functions/core/app_menubar.cpp (menu item)"
---
# log_window
Visualizador ImGui del ring buffer de `fn_log`. Render automatico via `fn::run_app`.
## UI
- **Toolbar superior:** checkboxes para mostrar/ocultar Debug, Info, Warn, Error; campo de busqueda por substring; checkbox de autoscroll; boton `Clear` para vaciar el buffer in-memory; al lado, la ruta del archivo de log activo (si hay).
- **Region central:** lista de lineas formateadas, coloreadas por nivel (gris/blanco/ambar/rojo).
- **Footer:** contador de entradas vs capacidad del buffer (2000 por defecto).
## Apertura
- Submenu **Settings -> Logs...** del menubar canonico (via `app_menubar`).
- Programaticamente: `fn_ui::log_window_set_open(true)`.
## Reglas
- No toca el archivo en disco; solo lee el buffer in-memory de `fn_log`.
- `Clear` borra el buffer pero NO trunca el archivo.
- Funciona aunque la app no haya llamado `logger_init` (el buffer existe siempre).
- Single-threaded como cualquier ventana ImGui — no llamar `log_window_render` desde otro hilo.
+156
View File
@@ -0,0 +1,156 @@
#include "core/logger.h"
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <mutex>
#include <string>
namespace fn_log {
namespace {
std::mutex g_mu;
FILE* g_file = nullptr;
std::string g_path;
Level g_min_level = Level::Info;
// Ring buffer in-memory. g_count es el numero de entradas vivas (clamp a
// capacity); g_head es el indice donde se escribira la proxima entrada.
Entry g_buf[kBufferCapacity];
std::size_t g_count = 0;
std::size_t g_head = 0;
long long now_ms() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
// Formato: "YYYY-MM-DD HH:MM:SS.mmm". out debe tener al menos 24 bytes.
void format_ts(long long ts_ms, char* out, std::size_t out_size) {
std::time_t secs = static_cast<std::time_t>(ts_ms / 1000);
int millis = static_cast<int>(ts_ms % 1000);
if (millis < 0) millis = 0;
if (millis > 999) millis = 999;
std::tm tm_buf{};
#ifdef _WIN32
localtime_s(&tm_buf, &secs);
#else
localtime_r(&secs, &tm_buf);
#endif
std::snprintf(out, out_size, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
(tm_buf.tm_year + 1900) % 10000,
(tm_buf.tm_mon + 1) % 100,
tm_buf.tm_mday % 100,
tm_buf.tm_hour % 100,
tm_buf.tm_min % 100,
tm_buf.tm_sec % 100,
millis);
}
void push_entry(Level level, long long ts_ms, const char* text) {
Entry& e = g_buf[g_head];
e.level = level;
e.ts_ms = ts_ms;
std::snprintf(e.text, kEntryTextMax, "%s", text);
g_head = (g_head + 1) % kBufferCapacity;
if (g_count < kBufferCapacity) ++g_count;
}
// Convierte el indice "logico" (0 = mas antigua) al indice fisico del array.
std::size_t logical_to_physical(std::size_t i) {
if (g_count < kBufferCapacity) return i; // buffer aun no lleno
return (g_head + i) % kBufferCapacity;
}
void emit(Level level, const char* fmt, std::va_list ap) {
if (static_cast<int>(level) < static_cast<int>(g_min_level)) return;
// msg deja 64 bytes para que el prefijo "[ts] [LEVEL] " quepa siempre en
// el buffer destino sin que -Wformat-truncation se queje.
char msg[kEntryTextMax - 64];
std::vsnprintf(msg, sizeof(msg), fmt, ap);
long long ts = now_ms();
char ts_buf[32];
format_ts(ts, ts_buf, sizeof(ts_buf));
char line[kEntryTextMax];
std::snprintf(line, sizeof(line), "[%s] [%s] %s",
ts_buf, level_label(level), msg);
std::lock_guard<std::mutex> lk(g_mu);
push_entry(level, ts, line);
if (g_file) {
std::fputs(line, g_file);
std::fputc('\n', g_file);
std::fflush(g_file);
}
}
} // namespace
bool logger_init(const char* file_path, Level min_level) {
std::lock_guard<std::mutex> lk(g_mu);
if (g_file) {
std::fclose(g_file);
g_file = nullptr;
}
g_min_level = min_level;
g_path.clear();
if (!file_path || !*file_path) return false;
g_file = std::fopen(file_path, "a");
if (!g_file) {
std::fprintf(stderr, "[fn_log] no pude abrir %s para append\n", file_path);
return false;
}
g_path = file_path;
return true;
}
void logger_close() {
std::lock_guard<std::mutex> lk(g_mu);
if (g_file) {
std::fclose(g_file);
g_file = nullptr;
}
g_path.clear();
}
void logger_set_level(Level level) { std::lock_guard<std::mutex> lk(g_mu); g_min_level = level; }
Level logger_level() { std::lock_guard<std::mutex> lk(g_mu); return g_min_level; }
const char* logger_path() { return g_path.c_str(); }
void log_debug(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Debug, fmt, ap); va_end(ap); }
void log_info (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Info, fmt, ap); va_end(ap); }
void log_warn (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Warn, fmt, ap); va_end(ap); }
void log_error(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Error, fmt, ap); va_end(ap); }
std::size_t buffer_size() { std::lock_guard<std::mutex> lk(g_mu); return g_count; }
const Entry* buffer_at(std::size_t i) {
std::lock_guard<std::mutex> lk(g_mu);
if (i >= g_count) return nullptr;
return &g_buf[logical_to_physical(i)];
}
void buffer_clear() {
std::lock_guard<std::mutex> lk(g_mu);
g_count = 0;
g_head = 0;
}
const char* level_label(Level level) {
switch (level) {
case Level::Debug: return "DEBUG";
case Level::Info: return "INFO";
case Level::Warn: return "WARN";
case Level::Error: return "ERROR";
}
return "?";
}
} // namespace fn_log
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <cstddef>
// Logger global thread-safe para apps del registry. Escribe a archivo (cwd
// junto al ejecutable, igual que app_settings.ini) y mantiene un ring buffer
// in-memory que el visualizador (log_window) consume.
//
// Lifecycle:
// - run_app llama logger_init(cfg.log_file, cfg.log_level) si log_file != nullptr
// - app llama log_info / log_warn / ... durante su ciclo de vida
// - run_app llama logger_close() al exit
//
// Apps que NO usan fn::run_app deben llamar logger_init/close manualmente.
// Si nunca se llama logger_init, los log_* siguen funcionando contra el ring
// buffer in-memory pero no escriben a disco.
//
// Formato de cada linea:
// [YYYY-MM-DD HH:MM:SS.mmm] [LEVEL] mensaje
namespace fn_log {
enum class Level : int {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
};
// Inicializa el logger global. file_path se interpreta relativo al cwd
// (donde la app ya escribe app_settings.ini). Crea/trunca el archivo si no
// existe; si existe, abre en modo append.
//
// Idempotente: si ya hay un archivo abierto, lo cierra y reabre el nuevo.
// Returns true si pudo abrir el archivo (false → solo buffer in-memory).
bool logger_init(const char* file_path, Level min_level = Level::Info);
// Cierra el archivo. log_* siguen funcionando contra el buffer in-memory.
void logger_close();
// Nivel minimo. Mensajes por debajo se descartan silenciosamente (no van ni
// al archivo ni al buffer).
void logger_set_level(Level level);
Level logger_level();
// Path del archivo activo. Vacio si no inicializado o cerrado.
const char* logger_path();
// Emisores. Formato printf-style. Cada llamada escribe una linea completa.
// Thread-safe (mutex interno).
void log_debug(const char* fmt, ...);
void log_info (const char* fmt, ...);
void log_warn (const char* fmt, ...);
void log_error(const char* fmt, ...);
// === Ring buffer in-memory (para log_window) ===
constexpr std::size_t kBufferCapacity = 2000;
constexpr std::size_t kEntryTextMax = 480; // deja sitio para timestamp + level
struct Entry {
Level level;
long long ts_ms; // unix epoch en milisegundos
char text[kEntryTextMax];
};
// Numero de entradas vivas en el buffer (≤ kBufferCapacity).
std::size_t buffer_size();
// Acceso por indice [0, buffer_size()). i==0 es la entrada mas antigua viva.
// Nullptr si i fuera de rango. Snapshot — el caller debe asumir que la
// entrada puede ser sobrescrita en la siguiente llamada thread-unsafe; para
// el viewer esto no es problema porque ImGui es single-threaded.
const Entry* buffer_at(std::size_t i);
// Limpia el ring buffer. No toca el archivo en disco.
void buffer_clear();
// Helper para el viewer: nombre corto del nivel ("DEBUG"/"INFO"/...).
const char* level_label(Level level);
} // namespace fn_log
+84
View File
@@ -0,0 +1,84 @@
---
name: logger
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "bool fn_log::logger_init(const char* file_path, fn_log::Level min_level = Level::Info); void fn_log::logger_close(); void fn_log::logger_set_level(Level); fn_log::Level fn_log::logger_level(); const char* fn_log::logger_path(); void fn_log::log_debug(const char* fmt, ...); void fn_log::log_info(const char* fmt, ...); void fn_log::log_warn(const char* fmt, ...); void fn_log::log_error(const char* fmt, ...); std::size_t fn_log::buffer_size(); const fn_log::Entry* fn_log::buffer_at(std::size_t); void fn_log::buffer_clear(); const char* fn_log::level_label(fn_log::Level)"
description: "Logger global thread-safe para apps C++ del registry. Escribe a archivo (cwd, junto al ejecutable) en modo append y mantiene un ring buffer in-memory de 2000 entradas que el visualizador log_window consume. Formato: [YYYY-MM-DD HH:MM:SS.mmm] [LEVEL] mensaje."
tags: [logger, logging, file, infra, thread-safe]
uses_functions: []
uses_types: [log_level_cpp_core, log_entry_cpp_core]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [chrono, cstdarg, cstdio, ctime, mutex, string]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/logger.cpp"
framework: ""
params:
- name: file_path
desc: "Ruta del archivo de log relativa al cwd (junto al ejecutable). Modo append. Si vacio o nullptr, no se escribe a disco — solo buffer in-memory"
- name: min_level
desc: "Nivel minimo a aceptar. Mensajes por debajo se descartan antes de tocar archivo o buffer"
- name: fmt
desc: "Formato printf-style de log_debug/info/warn/error. Cada llamada produce una linea independiente"
- name: i
desc: "Indice [0, buffer_size()) en el ring buffer. 0 = entrada mas antigua viva"
output: "logger_init retorna true si pudo abrir el archivo (false → solo buffer). logger_close cierra archivo (idempotente). log_* mutan estado global thread-safe. buffer_at retorna puntero valido o nullptr si i fuera de rango"
notes: "consumido por cpp/framework/app_base.cpp (init/close automatico via AppConfig.log) y cpp/functions/core/log_window.cpp (lectura del buffer)"
---
# logger
Logger global, thread-safe, integrado en `fn::run_app`: las apps solo declaran un `AppLogConfig` y emiten con `log_info(...)` etc.
## Uso desde una app
```cpp
#include "app_base.h"
#include "core/logger.h"
int main() {
return fn::run_app({
.title = "Mi App",
.log = {.file_path = "mi_app.log",
.level = static_cast<int>(fn_log::Level::Info)}
}, render);
}
```
Tras esto, `fn::run_app` llama `logger_init` antes del primer frame y `logger_close` al exit. La app solo necesita usar los emisores:
```cpp
fn_log::log_info ("usuario abrio archivo %s", path);
fn_log::log_warn ("retry %d/%d", attempt, max);
fn_log::log_error("connection failed: %s", reason);
fn_log::log_debug("estado interno: %d items", n);
```
## Uso sin fn::run_app
Apps que arman su propio main loop deben llamar manualmente:
```cpp
fn_log::logger_init("app.log", fn_log::Level::Info);
// ... vida de la app ...
fn_log::logger_close();
```
## Reglas
- Ruta relativa al cwd (igual convencion que `app_settings.ini`).
- Modo append: relanzar la app conserva el historico previo en disco.
- Thread-safe: un mutex interno protege archivo + buffer + nivel.
- Truncacion: cada mensaje cabe en `kEntryTextMax - 64` caracteres formateados; el resto se trunca silenciosamente.
- Si `logger_init` no se llama o falla, los `log_*` siguen siendo seguros: solo escriben al ring buffer in-memory (que la ventana `Logs` puede mostrar igualmente).
## Integracion
- `fn::AppConfig::log` activa el logger desde el framework.
- `fn_ui::log_window` lee el ring buffer y pinta la ventana "Logs..." del menubar.
+224
View File
@@ -0,0 +1,224 @@
#include "selectable_text.h"
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <string>
namespace fn_ui {
namespace {
// Empuja un estilo "frame transparente" para que InputText quede
// visualmente como Text() — sin borde, sin fondo, sin padding interno.
struct ScopedTransparentFrame {
ScopedTransparentFrame() {
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32(0,0,0,0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0, 0));
}
~ScopedTransparentFrame() {
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(3);
}
};
// Genera un id ImGui estable a partir del puntero del texto. Suficiente
// para evitar colisiones en el mismo frame mientras el caller no llame
// dos veces con la misma direccion.
void make_id(char* out, size_t n, const char* prefix, const char* text) {
std::snprintf(out, n, "##%s%p", prefix, (const void*)text);
}
} // namespace
// ----------------------------------------------------------------------------
// Versión wrapped (InputTextMultiline + read-only)
// ----------------------------------------------------------------------------
void selectable_text_wrapped(const char* text) {
if (!text) text = "";
if (!*text) {
ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
return;
}
float wrap_w = ImGui::GetContentRegionAvail().x;
if (wrap_w < 32.0f) wrap_w = 32.0f;
// CalcTextSize con wrap_w nos da la altura exacta del bloque renderizado.
ImVec2 sz = ImGui::CalcTextSize(text, nullptr, false, wrap_w);
if (sz.y < ImGui::GetTextLineHeight()) sz.y = ImGui::GetTextLineHeight();
// Pequeño margen para evitar que aparezca el scrollbar vertical.
float h = sz.y + 2.0f;
ScopedTransparentFrame _frame;
char id[40];
make_id(id, sizeof(id), "selw", text);
size_t len = std::strlen(text);
// ImGui requiere buffer no-const aunque ReadOnly impide escritura.
ImGui::InputTextMultiline(id,
const_cast<char*>(text), len + 1,
ImVec2(wrap_w, h),
ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_NoHorizontalScroll);
}
// ----------------------------------------------------------------------------
// Variante con wrap forzado (rompe palabras sin espacios)
// ----------------------------------------------------------------------------
namespace {
// Inserta '\n' donde el ancho acumulado de la linea supera wrap_w.
// Maneja UTF-8: no rompe en mitad de un codepoint multi-byte. Si la
// palabra actual cabe completa, espera al proximo separador; si no,
// fuerza un break al limite. Espacios y newlines existentes se
// respetan.
std::string force_wrap(const char* text, float wrap_w) {
std::string out;
if (!text || !*text || wrap_w <= 0.0f) {
if (text) out = text;
return out;
}
out.reserve(std::strlen(text) + std::strlen(text) / 32);
auto is_utf8_cont = [](unsigned char b) { return (b & 0xC0) == 0x80; };
auto width_of = [](const char* a, const char* b) -> float {
return ImGui::CalcTextSize(a, b).x;
};
const char* line_start = text;
const char* last_space = nullptr;
const char* p = text;
while (*p) {
if (*p == '\n') {
out.append(line_start, p + 1);
line_start = p + 1;
last_space = nullptr;
++p;
continue;
}
if (*p == ' ' || *p == '\t') last_space = p;
// Avanzar al final del codepoint UTF-8 actual.
const char* cp_end = p + 1;
while (*cp_end && is_utf8_cont((unsigned char)*cp_end)) ++cp_end;
float w = width_of(line_start, cp_end);
if (w > wrap_w && cp_end != line_start) {
if (last_space && last_space > line_start) {
out.append(line_start, last_space);
out.push_back('\n');
line_start = last_space + 1;
last_space = nullptr;
p = line_start;
continue;
}
// Sin espacio en la linea — break forzado al codepoint actual.
// Si solo hay un codepoint, evitamos un loop infinito
// dejando que se pinte aunque no quepa.
if (p == line_start) {
p = cp_end;
continue;
}
out.append(line_start, p);
out.push_back('\n');
line_start = p;
last_space = nullptr;
continue;
}
p = cp_end;
}
if (line_start < p) out.append(line_start, p);
return out;
}
} // namespace
void selectable_text_wrapped_force(const char* text) {
if (!text) text = "";
if (!*text) {
ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
return;
}
float wrap_w = ImGui::GetContentRegionAvail().x;
if (wrap_w < 32.0f) wrap_w = 32.0f;
// Margen para el padding interno del InputText — sin esto la
// ultima letra de una linea larga puede quedar tras el borde.
float effective_w = wrap_w - 4.0f;
std::string wrapped = force_wrap(text, effective_w);
ImVec2 sz = ImGui::CalcTextSize(wrapped.c_str(), nullptr, false, wrap_w);
if (sz.y < ImGui::GetTextLineHeight()) sz.y = ImGui::GetTextLineHeight();
float h = sz.y + 2.0f;
ScopedTransparentFrame _frame;
char id[40];
make_id(id, sizeof(id), "selwf", text);
ImGui::InputTextMultiline(id,
const_cast<char*>(wrapped.c_str()), wrapped.size() + 1,
ImVec2(wrap_w, h),
ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_NoHorizontalScroll);
}
void selectable_text_wrapped_fmt(const char* fmt, ...) {
char buf[4096];
va_list ap;
va_start(ap, fmt);
int n = std::vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n < 0) return;
selectable_text_wrapped(buf);
}
// ----------------------------------------------------------------------------
// Versión single-line (InputText + read-only)
// ----------------------------------------------------------------------------
void selectable_text(const char* text) {
if (!text) text = "";
if (!*text) return;
ScopedTransparentFrame _frame;
char id[40];
make_id(id, sizeof(id), "sel", text);
ImVec2 sz = ImGui::CalcTextSize(text);
ImGui::SetNextItemWidth(sz.x + 4.0f);
size_t len = std::strlen(text);
ImGui::InputText(id, const_cast<char*>(text), len + 1,
ImGuiInputTextFlags_ReadOnly
| ImGuiInputTextFlags_AutoSelectAll);
}
void selectable_text_fmt(const char* fmt, ...) {
char buf[1024];
va_list ap;
va_start(ap, fmt);
int n = std::vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n < 0) return;
selectable_text(buf);
}
// ----------------------------------------------------------------------------
// Versión ligera con right-click → Copy
// ----------------------------------------------------------------------------
void text_with_copy(const char* text) {
if (!text) text = "";
ImGui::TextUnformatted(text);
if (ImGui::BeginPopupContextItem("##copy_one")) {
if (ImGui::MenuItem("Copy")) ImGui::SetClipboardText(text);
ImGui::EndPopup();
}
}
void text_wrapped_with_copy(const char* text) {
if (!text) text = "";
ImGui::TextWrapped("%s", text);
if (ImGui::BeginPopupContextItem("##copy_one_w")) {
if (ImGui::MenuItem("Copy")) ImGui::SetClipboardText(text);
ImGui::EndPopup();
}
}
} // namespace fn_ui
+50
View File
@@ -0,0 +1,50 @@
#pragma once
//
// selectable_text — texto que el usuario puede seleccionar y copiar dentro
// de cualquier ventana ImGui. Reemplazo drop-in de ImGui::Text /
// ImGui::TextWrapped cuando se quiere permitir copia.
//
// ImGui::Text/TextWrapped son draw-list puro: no aceptan eventos de mouse.
// Para permitir seleccion de caracteres usamos InputTextMultiline con flag
// ReadOnly y el frame stylado a transparente — visualmente identico a
// TextWrapped pero arrastrable y copiable con Ctrl+C / boton derecho.
//
// Uso recomendado: en paneles donde el usuario quiere copiar contenido (chat,
// inspector, logs, notes view, output de queries, etc.). En toolbars y
// labels cortas no merece la pena cambiar — la version "TextCopy" basada en
// Text() + popup de boton derecho es mas ligera.
#include <cstdarg>
#include "imgui.h"
namespace fn_ui {
// Texto seleccionable con wrap automatico al ancho disponible (drop-in de
// ImGui::TextWrapped). Multi-linea. El usuario puede arrastrar para
// seleccionar y copiar con Ctrl+C o desde el menu contextual.
void selectable_text_wrapped(const char* text);
// Como selectable_text_wrapped pero forzando el wrap incluso para
// "palabras" que no tienen espacios (URLs largas, JSON, hashes). El
// InputTextMultiline base solo rompe en separadores; sin esto, el
// resto del token desaparece a la derecha. Esta variante pre-rompe el
// texto insertando saltos donde la linea supera el ancho disponible.
void selectable_text_wrapped_force(const char* text);
// Variante con printf-format. IM_FMTARGS(1) activa los warnings de
// formato del compilador.
void selectable_text_wrapped_fmt(const char* fmt, ...) IM_FMTARGS(1);
// Texto seleccionable de una sola linea (drop-in de ImGui::Text).
// Sin wrap. Para textos cortos / labels que el usuario quiere copiar.
void selectable_text(const char* text);
void selectable_text_fmt(const char* fmt, ...) IM_FMTARGS(1);
// Variante ligera: dibuja con ImGui::Text/TextWrapped y adjunta un popup
// "Copy" al boton derecho. NO permite seleccion de caracter, pero copia
// el texto completo de un click. Indicado para labels cortas, stats,
// status lines — nada que justifique el coste de InputText.
void text_with_copy(const char* text);
void text_wrapped_with_copy(const char* text);
} // namespace fn_ui
+82
View File
@@ -0,0 +1,82 @@
---
id: selectable_text_cpp_core
name: selectable_text
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: "void fn_ui::selectable_text_wrapped(const char* text)"
description: "Texto seleccionable y copiable (drag-to-select + Ctrl+C) para ventanas ImGui. Drop-in de ImGui::Text/TextWrapped cuando se quiere permitir copia. Tambien expone variantes ligeras con right-click → Copy."
tags: [imgui, text, selectable, clipboard, copy, accessibility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports:
- "imgui.h"
params:
- name: text
desc: "Texto a renderizar (UTF-8). Caller-owned, no copiado. El cast interno a char* es seguro porque ReadOnly impide escritura."
output: "Renderiza el texto en la ventana ImGui actual con drag-to-select y Ctrl+C habilitados. No retorna valor."
example: |
// En lugar de ImGui::TextWrapped("%s", chat_msg.c_str()):
fn_ui::selectable_text_wrapped(chat_msg.c_str());
// Single line (drop-in de ImGui::Text):
fn_ui::selectable_text(entity_id.c_str());
// Variante ligera (sin char-selection, copia el texto entero con
// boton derecho):
fn_ui::text_wrapped_with_copy(stats_line.c_str());
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/selectable_text.cpp"
framework: "imgui"
notes: |
ImGui::Text/TextWrapped son draw-list puro: no aceptan eventos de mouse, asi
que NO pueden seleccionarse. La unica via reliable hoy en ImGui (1.91.x) para
permitir char-level selection es usar InputTextMultiline con flag ReadOnly.
Esta funcion stylea el frame a transparente (sin borde, sin fondo, sin
padding, sin spacing) — visualmente identico a TextWrapped pero arrastrable.
Calcula la altura exacta con ImGui::CalcTextSize(text, NULL, false, wrap_w)
asi NO aparece scrollbar.
El cast `const_cast<char*>(text)` es seguro porque ImGui no escribe cuando
ReadOnly esta activo. La sobrecarga es minima (un InputText no es tan caro
como parece — ImGui ya cachea el layout entre frames).
Para textos muy largos (>10k chars) considera usar un InputTextMultiline
explicito con scrollbar visible — la altura calculada para "todo el bloque"
rompe el flujo de la ventana.
Cuando el usuario solo quiere copiar el texto entero (sin seleccionar parte),
text_with_copy() / text_wrapped_with_copy() son mas ligeros: dibujan con
Text() normal y abren un popup "Copy" con boton derecho.
documentation: |
## Diferencia con ImGui::Text
| Aspecto | ImGui::Text | fn_ui::selectable_text |
|---|---|---|
| Drag selection | No | Si |
| Ctrl+C | No | Si |
| Right-click | No | Menu de InputText |
| Coste | 1 DrawList ops | 1 InputText (cacheado) |
| Apariencia | Identica | Identica si frame transparente |
## Cuando usar cada variante
- **selectable_text_wrapped** — chat outputs, logs, descriptions, JSON dumps,
cualquier sitio donde el usuario podria querer copiar parte del texto.
- **selectable_text** — IDs cortos, paths, nombres tecnicos.
- **text_wrapped_with_copy** — stats lines, tooltips persistentes, status
messages. El usuario puede copiar todo de una vez sin seleccionar.
## Compatibilidad
Tambien funciona dentro de tablas ImGui (TableNextColumn → selectable_text).
No se rompe con docking/multi-viewport.
---
+32
View File
@@ -0,0 +1,32 @@
---
name: log_entry
lang: cpp
domain: core
version: "1.0.0"
algebraic: product
definition: |
namespace fn_log {
constexpr std::size_t kEntryTextMax = 480;
struct Entry {
Level level;
long long ts_ms; // unix epoch en milisegundos
char text[kEntryTextMax];
};
}
description: "Entrada del ring buffer in-memory del logger. Contiene nivel, timestamp en milisegundos y la linea ya formateada [ts] [LEVEL] mensaje."
tags: [logger, buffer, entry, ringbuffer]
uses_types: [log_level_cpp_core]
file_path: "cpp/functions/core/logger.h"
examples:
- "const fn_log::Entry* e = fn_log::buffer_at(0);"
notes: "POD trivialmente copiable. La ventana Logs (log_window) itera el buffer via buffer_at(i)."
---
# log_entry
Entrada individual del ring buffer in-memory del logger global. La capacidad del buffer es `fn_log::kBufferCapacity` (2000 entradas por defecto) — al llenarse, sobrescribe las mas antiguas.
`text` contiene la linea ya formateada (con prefijo `[YYYY-MM-DD HH:MM:SS.mmm] [LEVEL]`), lista para ser pintada por el visualizador. `level` y `ts_ms` se mantienen aparte para filtrado y coloreado por nivel.
Acceso de solo lectura via `fn_log::buffer_at(i)` con `i ∈ [0, fn_log::buffer_size())`. El indice 0 corresponde a la entrada mas antigua viva.
+37
View File
@@ -0,0 +1,37 @@
---
name: log_level
lang: cpp
domain: core
version: "1.0.0"
algebraic: sum
definition: |
namespace fn_log {
enum class Level : int {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
};
}
description: "Nivel de severidad de un mensaje de log. Ordenado por severidad ascendente: Debug < Info < Warn < Error."
tags: [logger, level, severity, enum]
uses_types: []
file_path: "cpp/functions/core/logger.h"
examples:
- "fn_log::Level::Info"
- "fn_log::Level::Error"
notes: "Usado por logger_init y logger_set_level. Mensajes con nivel < min_level se descartan."
---
# log_level
Nivel de severidad de los mensajes emitidos por el logger global de C++ del registry. Se mapea 1:1 con los niveles tipicos de cualquier sistema de logging.
| Valor | Cuando usar |
|---|---|
| `Debug` | Trazas detalladas de desarrollo. Suelen filtrarse en produccion. |
| `Info` | Eventos normales del flujo (start, exit, milestones). |
| `Warn` | Situaciones anomalas que no impiden el funcionamiento. |
| `Error` | Fallos que afectan a la operacion. |
El parametro `min_level` de `logger_init` filtra: cualquier mensaje con nivel inferior se descarta antes de tocar el archivo o el ring buffer. La ventana `Logs` permite ademas ocultar niveles ya capturados.