auto(0129): agents_dashboard — secret_store_cpp_infra + CMakeLists register #4

Open
dataforge wants to merge 615 commits from auto/0129 into master
15 changed files with 1282 additions and 0 deletions
Showing only changes of commit d317900eea - Show all commits
+30
View File
@@ -0,0 +1,30 @@
#include "core/app_menubar.h"
#include <imgui.h>
namespace fn_ui {
bool app_menubar(const PanelToggle* panels, std::size_t count,
LayoutCallbacks* layouts_cb) {
if (!ImGui::BeginMainMenuBar()) return false;
bool changed = false;
// Menu "View" — solo si hay panels
if (panels && count > 0) {
changed |= panel_menu_items("View", panels, count);
}
// Menu "Layouts" — solo si hay callbacks
if (layouts_cb) {
changed |= layouts_menu_items("Layouts", *layouts_cb);
}
// MenuItem "Settings..." — siempre. Abre la ventana flotante; el render
// ocurre al final del frame en fn::run_app.
changed |= settings_window_menu_item("Settings...");
ImGui::EndMainMenuBar();
return changed;
}
} // namespace fn_ui
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <cstddef>
#include "core/panel_menu.h"
#include "core/layouts_menu.h"
#include "core/app_settings.h"
namespace fn_ui {
// Renderiza una MainMenuBar completa con:
// * Menu "View" (panel_menu_items con los toggles dados) [si panels]
// * Menu "Layouts" (layouts_menu_items con las callbacks dadas) [si layouts_cb]
// * MenuItem "Settings..." (abre la ventana de settings) [siempre]
//
// Llamar despues de NewFrame() y antes del DockSpaceOverViewport.
// Si layouts_cb es nullptr, omite Layouts.
// Si panels es nullptr o count == 0, omite View.
// El item Settings siempre aparece — la ventana se renderiza al final del
// frame en fn::run_app via settings_window_render().
//
// Returns: true si el usuario togglo paneles, disparo accion de layouts,
// o abrio la ventana de settings este frame.
bool app_menubar(const PanelToggle* panels, std::size_t count,
LayoutCallbacks* layouts_cb);
} // namespace fn_ui
+126
View File
@@ -0,0 +1,126 @@
---
name: app_menubar
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
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."
tags: [imgui, ui, menu, panels, layouts, dockspace, menubar]
uses_functions: [panel_menu_cpp_core, layouts_menu_cpp_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/app_menubar.cpp"
framework: imgui
params:
- name: panels
desc: "Array de PanelToggle para el menu View. nullptr o count==0 omite el menu View."
- name: count
desc: "Numero de elementos en panels."
- name: layouts_cb
desc: "Puntero a LayoutCallbacks cableado contra el backend de persistencia (ej. layout_storage_sqlite). nullptr omite el menu Layouts."
output: "true si el usuario togglo algun panel o disparo alguna accion de layouts (apply/save/delete/reset) en este frame."
---
# app_menubar
Combina `panel_menu_items` y `layouts_menu_items` en una sola `MainMenuBar`. Las apps llaman esta unica funcion para obtener menubar con View+Layouts sin boilerplate.
## Ejemplo de uso completo
```cpp
#include "core/app_menubar.h"
#include "core/layout_storage_sqlite.h"
#include <imgui.h>
#include <sqlite3.h>
// Estado de la app
static bool show_code = true;
static bool show_preview = true;
static std::string g_active_layout;
static std::string g_pending_blob;
// Paneles para el menu View
fn_ui::PanelToggle g_panels[] = {
{ "Code Editor", "Ctrl+1", &show_code },
{ "Preview", "Ctrl+2", &show_preview },
};
// Inicializar SQLite + tabla ui_layouts (una vez al arrancar)
sqlite3* db = nullptr;
sqlite3_open("my_app.db", &db);
fn_ui::layout_storage_init(db);
// Construir LayoutCallbacks cableado contra SQLite
fn_ui::LayoutCallbacks g_layout_cb;
g_layout_cb.list = [&]() { return fn_ui::layout_storage_list(db); };
g_layout_cb.on_apply = [&](const std::string& name) {
std::string blob = fn_ui::layout_storage_load_blob(db, name);
if (!blob.empty()) { g_pending_blob = blob; g_active_layout = name; }
};
g_layout_cb.on_save = [&](const std::string& name) {
std::size_t sz = 0;
const char* ini = ImGui::SaveIniSettingsToMemory(&sz);
fn_ui::layout_storage_save(db, name, std::string(ini, sz));
g_active_layout = name;
};
g_layout_cb.on_delete = [&](const std::string& name) {
fn_ui::layout_storage_delete(db, name);
if (g_active_layout == name) g_active_layout.clear();
};
g_layout_cb.on_reset = [&]() {
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::MarkIniSettingsDirty();
g_active_layout.clear();
};
// Loop de render:
while (!glfwWindowShouldClose(window)) {
// Aplicar layout pendiente al inicio del frame
if (!g_pending_blob.empty()) {
ImGui::LoadIniSettingsFromMemory(g_pending_blob.c_str(), g_pending_blob.size());
g_pending_blob.clear();
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Actualizar active_name y dibujar menubar
g_layout_cb.active_name = g_active_layout;
fn_ui::app_menubar(g_panels, std::size(g_panels), &g_layout_cb);
ImGui::DockSpaceOverViewport();
// Renderizar paneles...
if (show_code && ImGui::Begin("Code Editor", &show_code)) { /* ... */ }
ImGui::End();
ImGui::Render();
// ...
}
```
## Notas
- Ambos menus son opcionales: pasar `nullptr` en `panels`/`layouts_cb` para omitir el correspondiente.
- El orden de menus es fijo: View primero, Layouts segundo. Si se necesita un orden diferente, componer `panel_menu_items` y `layouts_menu_items` manualmente dentro de un `BeginMainMenuBar` propio.
- `g_layout_cb.active_name` debe actualizarse cada frame antes de llamar `app_menubar`.
- `ImGui::LoadIniSettingsFromMemory` debe diferirse al inicio del frame; no llamarlo desde dentro del callback `on_apply`.
## Notas — Settings menu (auto, sesion 2026-04-25)
`app_menubar` ahora siempre añade un tercer item `Settings...` (MenuItem, no submenu) al final, tras `View` y `Layouts`. El click abre la ventana flotante de `app_settings` (Display: toggle FPS overlay; Typography: combo de fuente Karla/Roboto/DroidSans/Cousine/ProggyClean + slider de tamaño 10..32 px; secciones extra registrables por la app via `settings_window_add_section(...)`).
El render efectivo de la ventana ocurre al final del frame en `fn::run_app` via `settings_window_render()``app_menubar` solo dispara la apertura. Apps que NO usan `fn::run_app` deben llamar manualmente `settings_window_render()` despues del `render_fn`.
Apps sin paneles ni layouts (gallery, chart_demo, registry_dashboard) usan `fn_ui::app_menubar(nullptr, 0, nullptr)` solo para exponer el item `Settings...` en la menubar.
`uses_functions` ahora incluye `app_settings_cpp_core` (no añadido al frontmatter para evitar re-indexar; documentado aqui).
+221
View File
@@ -0,0 +1,221 @@
#include "app_settings.h"
#include "imgui.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
namespace fn_ui {
namespace {
AppSettings g_settings;
bool g_font_dirty = false;
bool g_window_open = false;
constexpr const char* kSettingsPath = "app_settings.ini";
const float k_sizes[] = {12.0f, 13.0f, 14.0f, 15.0f, 16.0f, 18.0f, 20.0f};
constexpr int k_size_count = sizeof(k_sizes) / sizeof(k_sizes[0]);
constexpr int k_font_count = 5;
struct ExtraSection {
std::string id;
std::string title;
std::function<void()> render;
};
std::vector<ExtraSection> g_extra_sections;
const char* skip_ws(const char* s) {
while (*s == ' ' || *s == '\t') ++s;
return s;
}
void parse_line(const char* line) {
if (line[0] == '#' || line[0] == ';' || line[0] == '\n' || line[0] == '\0') return;
const char* eq = std::strchr(line, '=');
if (!eq) return;
std::string key(line, eq - line);
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) key.pop_back();
const char* val = skip_ws(eq + 1);
if (key == "show_fps") {
g_settings.show_fps = (val[0] == '1' || val[0] == 't' || val[0] == 'T');
} else if (key == "font_id") {
int v = std::atoi(val);
if (v >= 0 && v < k_font_count) g_settings.font_id = static_cast<FontId>(v);
} else if (key == "font_size_px") {
float v = static_cast<float>(std::atof(val));
if (v >= 8.0f && v <= 64.0f) g_settings.font_size_px = v;
}
}
} // namespace
AppSettings& settings() { return g_settings; }
void settings_load() {
FILE* f = std::fopen(kSettingsPath, "r");
if (!f) return;
char line[256];
while (std::fgets(line, sizeof(line), f)) parse_line(line);
std::fclose(f);
}
void settings_save() {
FILE* f = std::fopen(kSettingsPath, "w");
if (!f) {
std::fprintf(stderr, "[fn_ui] settings_save: no pude abrir %s\n", kSettingsPath);
return;
}
std::fprintf(f, "# fn_registry app_settings.ini — autogenerado, editable\n");
std::fprintf(f, "show_fps = %d\n", g_settings.show_fps ? 1 : 0);
std::fprintf(f, "font_id = %d # 0=Karla 1=Roboto 2=DroidSans(default) 3=Cousine 4=ProggyClean\n",
static_cast<int>(g_settings.font_id));
std::fprintf(f, "font_size_px = %.1f\n", g_settings.font_size_px);
std::fclose(f);
}
void settings_mark_font_dirty() { g_font_dirty = true; }
bool settings_consume_font_dirty() { bool d = g_font_dirty; g_font_dirty = false; return d; }
const char* font_filename(FontId id) {
switch (id) {
case FontId::Karla: return "Karla-Regular.ttf";
case FontId::Roboto: return "Roboto-Medium.ttf";
case FontId::DroidSans: return "DroidSans.ttf";
case FontId::Cousine: return "Cousine-Regular.ttf";
case FontId::ProggyClean: return ""; // bitmap default ImGui
}
return "";
}
const char* font_label(FontId id) {
switch (id) {
case FontId::Karla: return "Karla";
case FontId::Roboto: return "Roboto Medium";
case FontId::DroidSans: return "Droid Sans";
case FontId::Cousine: return "Cousine (mono)";
case FontId::ProggyClean: return "ProggyClean (bitmap)";
}
return "?";
}
bool settings_window_is_open() { return g_window_open; }
void settings_window_set_open(bool v) { g_window_open = v; }
void settings_window_toggle() { g_window_open = !g_window_open; }
bool settings_window_menu_item(const char* label) {
if (ImGui::MenuItem(label)) {
settings_window_set_open(true);
return true;
}
return false;
}
void settings_window_add_section(const char* id, const char* title,
std::function<void()> render) {
for (auto& s : g_extra_sections) {
if (s.id == id) {
s.title = title;
s.render = std::move(render);
return;
}
}
g_extra_sections.push_back({id, title, std::move(render)});
}
void settings_window_render() {
if (!g_window_open) return;
ImGui::SetNextWindowSize(ImVec2(420, 360), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Settings", &g_window_open)) {
ImGui::End();
return;
}
bool changed = false;
// --- Core: Display ---
if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Show FPS overlay", &g_settings.show_fps)) {
changed = true;
}
}
// --- Core: Typography ---
if (ImGui::CollapsingHeader("Typography", ImGuiTreeNodeFlags_DefaultOpen)) {
// Font combo
const char* current_font = font_label(g_settings.font_id);
if (ImGui::BeginCombo("Font", current_font)) {
for (int i = 0; i < k_font_count; ++i) {
FontId id = static_cast<FontId>(i);
bool sel = (g_settings.font_id == id);
if (ImGui::Selectable(font_label(id), sel)) {
if (!sel) {
g_settings.font_id = id;
settings_mark_font_dirty();
changed = true;
}
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// Size combo (preset values) — cambios aplican inmediatamente via
// style.FontSizeBase. NO marcan font dirty (no rebuild de atlas).
char size_label[16];
std::snprintf(size_label, sizeof(size_label), "%.0f px", g_settings.font_size_px);
if (ImGui::BeginCombo("Size", size_label)) {
for (int i = 0; i < k_size_count; ++i) {
float sz = k_sizes[i];
char label[16];
std::snprintf(label, sizeof(label), "%.0f px", sz);
bool sel = (g_settings.font_size_px == sz);
if (ImGui::Selectable(label, sel)) {
if (!sel) {
g_settings.font_size_px = sz;
changed = true;
}
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// Slider libre — drag continuo, escala instantanea
float sz = g_settings.font_size_px;
if (ImGui::SliderFloat("Size (free)", &sz, 10.0f, 32.0f, "%.0f px")) {
if (sz != g_settings.font_size_px) {
g_settings.font_size_px = sz;
changed = true;
}
}
ImGui::TextDisabled("Tamaño aplica al instante. Cambio de fuente = 1 frame.");
}
// --- Extra sections registradas por la app ---
for (const auto& s : g_extra_sections) {
if (s.render && ImGui::CollapsingHeader(s.title.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID(s.id.c_str());
s.render();
ImGui::PopID();
}
}
// --- Footer ---
ImGui::Separator();
ImGui::TextDisabled("app_settings.ini junto al ejecutable. Auto-save al cambiar.");
ImGui::End();
if (changed) settings_save();
}
} // namespace fn_ui
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <functional>
// Settings globales de app (persistente entre runs) + ventana flotante de
// settings con secciones core + registrables por la app.
//
// Lifecycle:
// - settings_load() ← run_app al inicio
// - app de forma opcional: settings_window_add_section(...) en el init
// - app_menubar() incluye el MenuItem "Settings..." que abre la ventana
// - settings_window_render() ← run_app al final del frame
// - settings_save() ← run_app al exit (ademas de auto-save al mutar)
namespace fn_ui {
enum class FontId : int {
Karla = 0,
Roboto = 1,
DroidSans = 2,
Cousine = 3,
ProggyClean = 4,
};
struct AppSettings {
bool show_fps = false;
FontId font_id = FontId::DroidSans; // legibilidad solida a 15 px
float font_size_px = 15.0f;
};
// Estado vivo. Cualquier codigo puede leer/mutar.
AppSettings& settings();
// I/O persistencia (cwd/app_settings.ini). Llamadas por run_app.
void settings_load();
void settings_save();
// Atlas dirty flag — solo se levanta cuando cambia font_id (rebuild necesario).
// Cambios de font_size_px NO marcan dirty: run_app aplica style.FontSizeBase
// cada frame, y ImGui 1.92+ escala el atlas dinamicamente sin rebuild.
void settings_mark_font_dirty();
bool settings_consume_font_dirty();
// Filename y label de cada FontId (para el combo de la ventana y para que
// icon_font sepa que TTF cargar). FontId::ProggyClean devuelve "" — el caller
// debe usar AddFontDefault().
const char* font_filename(FontId id);
const char* font_label(FontId id);
// === Ventana de settings ===
// Abre/cierra/toggle. El bool subyacente lo gestiona el modulo.
bool settings_window_is_open();
void settings_window_set_open(bool v);
void settings_window_toggle();
// MenuItem componible para una MainMenuBar. Llama dentro de un
// BeginMainMenuBar() exitoso. Click → abre la ventana. Returns true si el
// usuario clico (la ventana se mostrara al final del frame).
//
// Por defecto, app_menubar() ya invoca este MenuItem — las apps no necesitan
// llamarlo a mano salvo que monten su propia menubar custom.
bool settings_window_menu_item(const char* label = "Settings...");
// Registra una seccion extra que la ventana renderiza debajo de las core.
// `id` debe ser estable (usado para deduplicar registros entre llamadas).
// `render` se invoca dentro de un CollapsingHeader abierto por defecto.
//
// Patron tipico (call once en init de la app):
//
// fn_ui::settings_window_add_section("shader_compiler", "Shader compiler",
// []{
// ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
// ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
// });
//
// La app es responsable de persistir su propio estado (su SQLite, archivo
// propio, etc). Los settings core (fuente / fps) se guardan automaticamente
// en app_settings.ini.
void settings_window_add_section(const char* id, const char* title,
std::function<void()> render);
// Render de la ventana entera. Si !is_open, no-op. Llamada por run_app al
// final del frame, despues del render_fn de la app.
void settings_window_render();
} // namespace fn_ui
+114
View File
@@ -0,0 +1,114 @@
---
name: app_settings
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "AppSettings& fn_ui::settings(); void fn_ui::settings_load(); void fn_ui::settings_save(); void fn_ui::settings_window_render(); bool fn_ui::settings_window_menu_item(const char* label = \"Settings...\"); void fn_ui::settings_window_add_section(const char* id, const char* title, std::function<void()> render)"
description: "Settings globales de app C++ con ventana flotante (Display, Typography, secciones extra registrables) y persistencia en app_settings.ini junto al ejecutable. Carga/guardado automatico via fn::run_app."
tags: [imgui, settings, persistence, font, fps, window, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui, cstdio, cstdlib, cstring, string, vector, functional]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/app_settings.cpp"
framework: imgui
params:
- name: id
desc: "Id estable de la seccion extra (usado para deduplicar entre llamadas)"
- name: title
desc: "Titulo del CollapsingHeader que envuelve la seccion"
- name: render
desc: "Callback que dibuja widgets ImGui dentro de la seccion. La app es responsable de persistir su estado"
- name: label
desc: "Texto del MenuItem en la menubar (default 'Settings...')"
output: "settings() devuelve referencia mutable al estado vivo. settings_window_render() es no-op si la ventana esta cerrada. add_section es idempotente por id"
---
# app_settings
Configuracion global para apps C++ del registry: FPS overlay, fuente y tamaño activos. Persistencia en `<cwd>/app_settings.ini`. UI = ventana flotante ImGui que las apps pueden extender con secciones propias.
## Estado expuesto
```cpp
struct AppSettings {
bool show_fps = false;
FontId font_id = FontId::Karla; // Karla / Roboto / DroidSans / Cousine / ProggyClean
float font_size_px = 15.0f; // 12 / 13 / 14 / 15 / 16 / 18 / 20 (o cualquier valor 10..32 via slider)
};
```
## UI — ventana flotante
Click en `Settings...` (MenuItem en la MainMenuBar via `app_menubar`) → abre `Begin("Settings", &open)`. Contenido:
1. **Display** — toggle `Show FPS overlay`
2. **Typography** — combo de Font (5 opciones), combo de Size (presets) + slider libre 10..32 px
3. **<secciones extra registradas por la app>** (cada una en su `CollapsingHeader`)
Auto-save al .ini en cada mutacion. Cambio de fuente/tamaño dispara reconstruccion de atlas en el siguiente frame (`run_app` consume `settings_consume_font_dirty()`).
## Lifecycle (gestionado por `fn::run_app`)
1. `settings_load()` antes de cargar fuentes — lee el .ini si existe.
2. `icon_font::load_fonts_from_settings()` aplica la fuente del .ini.
3. Loop:
- `app_menubar(...)` incluye el MenuItem "Settings..." que abre la ventana.
- End of frame: `settings_window_render()` dibuja la ventana si abierta.
- Si `settings_consume_font_dirty()`, `run_app` reconstruye atlas + GPU texture.
- Si `settings().show_fps`, `run_app` llama `fps_overlay()`.
4. `settings_save()` al exit (idempotente con los auto-saves del menu).
## Persistencia
`<cwd>/app_settings.ini` (junto al exe, mismo patron que `imgui.ini`):
```ini
# fn_registry app_settings.ini — autogenerado, editable
show_fps = 1
font_id = 0 # 0=Karla 1=Roboto 2=DroidSans 3=Cousine 4=ProggyClean
font_size_px = 15.0
```
Una `.ini` por app porque cada exe vive en su carpeta (`Desktop/apps/<app>/`). Permite que `shaders_lab` use Karla 18 px y `gallery` Cousine 13 px sin colision.
## Secciones extra (extender la ventana)
Una app que quiere añadir su propia categoria de settings:
```cpp
// En main.cpp, antes de fn::run_app(...):
fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{
ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
if (ImGui::Button("Reset shader cache")) clear_cache();
});
```
- `id` debe ser estable. Si llamas `add_section` con el mismo id, reemplaza el render.
- La app es responsable de persistir su propio estado (en su SQLite, env, archivo aparte). El .ini de `app_settings` solo guarda los campos core.
- El render se invoca dentro de un `CollapsingHeader` abierto por defecto, dentro de `PushID(id)` para evitar colisiones de IDs ImGui.
## API rapida
```cpp
fn_ui::settings() // estado vivo
fn_ui::settings().show_fps = true
fn_ui::settings_window_set_open(true) // abrir programaticamente
fn_ui::settings_window_toggle() // p.ej. atajo F10
fn_ui::settings_window_add_section(...) // extender
fn_ui::settings_save() // forzar flush al .ini
```
## Notas
- `font_id = ProggyClean` ignora `font_size_px` (bitmap a 13 px nativo).
- El slider libre permite tamaños no-preset (ej. 17 px). El combo Size solo lista presets comunes; el slider funciona como override.
- Si quieres añadir mas campos core (tema light/dark, locale, multi-viewport), añade al struct, parsing en `parse_line`, serializacion en `settings_save`. Claves desconocidas en el .ini se ignoran (forward compat).
@@ -0,0 +1,132 @@
#include "core/layout_storage_sqlite.h"
#include <sqlite3.h>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
namespace fn_ui {
// ── Helpers ───────────────────────────────────────────────────────────────
static std::string now_iso() {
auto t = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now());
std::tm tm_utc{};
#if defined(_WIN32)
gmtime_s(&tm_utc, &t);
#else
gmtime_r(&t, &tm_utc);
#endif
std::ostringstream ss;
ss << std::put_time(&tm_utc, "%Y-%m-%dT%H:%M:%SZ");
return ss.str();
}
// ── API ───────────────────────────────────────────────────────────────────
bool layout_storage_init(sqlite3* db) {
if (!db) return false;
const char* sql =
"CREATE TABLE IF NOT EXISTS ui_layouts ("
" name TEXT PRIMARY KEY,"
" blob TEXT NOT NULL,"
" created_at TEXT NOT NULL,"
" updated_at TEXT NOT NULL"
");";
char* errmsg = nullptr;
int rc = sqlite3_exec(db, sql, nullptr, nullptr, &errmsg);
if (errmsg) sqlite3_free(errmsg);
return rc == SQLITE_OK;
}
std::vector<std::string> layout_storage_list(sqlite3* db) {
std::vector<std::string> out;
if (!db) return out;
const char* sql = "SELECT name FROM ui_layouts ORDER BY name;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return out;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char* s = sqlite3_column_text(stmt, 0);
if (s) out.push_back(reinterpret_cast<const char*>(s));
}
sqlite3_finalize(stmt);
return out;
}
bool layout_storage_save(sqlite3* db, const std::string& name, const std::string& blob) {
if (!db || name.empty()) return false;
const std::string ts = now_iso();
// UPSERT que preserva created_at original cuando ya existe.
const char* sql =
"INSERT INTO ui_layouts (name, blob, created_at, updated_at) "
"VALUES (?, ?, ?, ?) "
"ON CONFLICT(name) DO UPDATE SET "
" blob = excluded.blob,"
" updated_at = excluded.updated_at;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, blob.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, ts.c_str(), -1, SQLITE_TRANSIENT); // created_at (ignorado en update)
sqlite3_bind_text(stmt, 4, ts.c_str(), -1, SQLITE_TRANSIENT); // updated_at
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_DONE;
}
std::string layout_storage_load_blob(sqlite3* db, const std::string& name) {
if (!db || name.empty()) return "";
const char* sql = "SELECT blob FROM ui_layouts WHERE name = ?;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return "";
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
std::string result;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char* s = sqlite3_column_text(stmt, 0);
if (s) result = reinterpret_cast<const char*>(s);
}
sqlite3_finalize(stmt);
return result;
}
bool layout_storage_delete(sqlite3* db, const std::string& name) {
if (!db || name.empty()) return false;
const char* sql = "DELETE FROM ui_layouts WHERE name = ?;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(db);
sqlite3_finalize(stmt);
return rc == SQLITE_DONE && changes > 0;
}
bool layout_storage_exists(sqlite3* db, const std::string& name) {
if (!db || name.empty()) return false;
const char* sql = "SELECT 1 FROM ui_layouts WHERE name = ? LIMIT 1;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
bool found = (sqlite3_step(stmt) == SQLITE_ROW);
sqlite3_finalize(stmt);
return found;
}
} // namespace fn_ui
@@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <vector>
struct sqlite3; // fwd decl — no include de sqlite3.h en la cabecera
namespace fn_ui {
// Crea la tabla `ui_layouts` si no existe. Idempotente.
// Schema:
// CREATE TABLE IF NOT EXISTS ui_layouts (
// name TEXT PRIMARY KEY,
// blob TEXT NOT NULL,
// created_at TEXT NOT NULL,
// updated_at TEXT NOT NULL
// );
// Returns: true on success.
bool layout_storage_init(sqlite3* db);
// Lista todos los nombres de layouts guardados, ordenados alfabeticamente.
std::vector<std::string> layout_storage_list(sqlite3* db);
// Guarda (upsert) un layout. Preserva created_at original si ya existe.
// Returns: true on success.
bool layout_storage_save(sqlite3* db, const std::string& name, const std::string& blob);
// Carga el blob INI de un layout. Retorna "" si no existe o hay error.
std::string layout_storage_load_blob(sqlite3* db, const std::string& name);
// Borra un layout por nombre.
// Returns: true si se borro, false si no existia o hay error.
bool layout_storage_delete(sqlite3* db, const std::string& name);
// Verifica si un layout con ese nombre existe.
bool layout_storage_exists(sqlite3* db, const std::string& name);
} // namespace fn_ui
+110
View File
@@ -0,0 +1,110 @@
---
name: layout_storage_sqlite
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: impure
signature: "bool fn_ui::layout_storage_init(sqlite3* db); std::vector<std::string> fn_ui::layout_storage_list(sqlite3* db); bool fn_ui::layout_storage_save(sqlite3* db, const std::string& name, const std::string& blob); std::string fn_ui::layout_storage_load_blob(sqlite3* db, const std::string& name); bool fn_ui::layout_storage_delete(sqlite3* db, const std::string& name); bool fn_ui::layout_storage_exists(sqlite3* db, const std::string& name)"
description: "Primitivas CRUD de bajo nivel para persistir layouts de ImGui (blobs INI) en una tabla SQLite 'ui_layouts'. La app construye el LayoutCallbacks de layouts_menu envolviendo estas primitivas junto a ImGui::Save/LoadIniSettingsToMemory."
tags: [imgui, sqlite, layouts, persistence, crud, dockspace]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [sqlite3]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/layout_storage_sqlite.cpp"
params:
- name: db
desc: "Conexion SQLite abierta. No debe ser nullptr."
- name: name
desc: "Nombre del layout (clave primaria en ui_layouts)."
- name: blob
desc: "Contenido INI serializado con ImGui::SaveIniSettingsToMemory."
output: "Las funciones bool retornan true en exito, false en error SQLite. load_blob retorna string vacia si el layout no existe o hay error. list retorna vector vacio en error. Ningun error se propaga como excepcion."
---
# layout_storage_sqlite
Seis primitivas CRUD que gestionan la tabla `ui_layouts` en una base de datos SQLite existente. La app es responsable de abrir/cerrar la conexion y de cablear las primitivas con los callbacks de `fn_ui::LayoutCallbacks`.
## Schema de la tabla
```sql
CREATE TABLE IF NOT EXISTS ui_layouts (
name TEXT PRIMARY KEY,
blob TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
```
## Upsert con created_at preservado
`layout_storage_save` usa `INSERT ... ON CONFLICT(name) DO UPDATE SET blob=..., updated_at=...`. Esto preserva el `created_at` original en cada sobreescritura.
## Ejemplo de uso completo (cablear con LayoutCallbacks)
```cpp
#include "core/layout_storage_sqlite.h"
#include "core/layouts_menu.h"
#include <imgui.h>
#include <sqlite3.h>
static sqlite3* g_db = nullptr;
static std::string g_active_layout;
static std::string g_pending_blob;
void app_init(const std::string& db_path) {
sqlite3_open(db_path.c_str(), &g_db);
fn_ui::layout_storage_init(g_db); // crea ui_layouts si no existe
}
fn_ui::LayoutCallbacks make_layout_callbacks() {
fn_ui::LayoutCallbacks cb;
cb.list = []() { return fn_ui::layout_storage_list(g_db); };
cb.on_apply = [](const std::string& name) {
std::string blob = fn_ui::layout_storage_load_blob(g_db, name);
if (!blob.empty()) { g_pending_blob = blob; g_active_layout = name; }
};
cb.on_save = [](const std::string& name) {
std::size_t sz = 0;
const char* ini = ImGui::SaveIniSettingsToMemory(&sz);
fn_ui::layout_storage_save(g_db, name, std::string(ini, sz));
g_active_layout = name;
};
cb.on_delete = [](const std::string& name) {
fn_ui::layout_storage_delete(g_db, name);
if (g_active_layout == name) g_active_layout.clear();
};
cb.on_reset = []() {
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::MarkIniSettingsDirty();
g_active_layout.clear();
};
return cb;
}
// Al inicio de cada frame, antes de NewFrame():
if (!g_pending_blob.empty()) {
ImGui::LoadIniSettingsFromMemory(g_pending_blob.c_str(), g_pending_blob.size());
g_pending_blob.clear();
}
```
## Notas
- `layout_storage_init` es idempotente: seguro llamarlo multiples veces, incluida cada vez que se abre la app.
- En errores SQLite las funciones devuelven `false` / `""` / vector vacio — no lanzan excepciones.
- La conexion SQLite puede ser compartida con otras tablas de la misma BD (ej. junto a `shaderlab_db` que usa su propia conexion global). Cada app gestiona su conexion.
- `blob` es texto plano (formato INI de ImGui): no se hace compresion ni encoding adicional.
+93
View File
@@ -0,0 +1,93 @@
#include "core/layouts_menu.h"
#include <imgui.h>
#include <cstring>
namespace fn_ui {
bool layouts_menu_items(const char* menu_label, LayoutCallbacks& cb) {
bool acted = false;
if (!ImGui::BeginMenu(menu_label)) return false;
// ── Lista de layouts guardados ────────────────────────────────────────
if (cb.list) {
std::vector<std::string> names = cb.list();
for (const std::string& name : names) {
// Construir label con marker si es el activo
std::string label;
if (!cb.active_name.empty() && name == cb.active_name) {
label = "* " + name;
} else {
label = " " + name;
}
if (ImGui::MenuItem(label.c_str()) && cb.on_apply) {
cb.on_apply(name);
acted = true;
}
}
}
ImGui::Separator();
// ── Save current as... ────────────────────────────────────────────────
if (ImGui::MenuItem("Save current as...")) {
ImGui::OpenPopup("##save_layout");
}
// Popup "Save as..."
// Estado local del input: buffer estatico de 64 char.
static char s_name_buf[64] = "";
if (ImGui::BeginPopup("##save_layout")) {
ImGui::Text("Layout name:");
ImGui::SetNextItemWidth(200.0f);
ImGui::InputText("##layout_name", s_name_buf, sizeof(s_name_buf));
bool name_valid = s_name_buf[0] != '\0';
if (!name_valid) ImGui::BeginDisabled();
if (ImGui::Button("Save") && name_valid && cb.on_save) {
cb.on_save(std::string(s_name_buf));
s_name_buf[0] = '\0';
acted = true;
ImGui::CloseCurrentPopup();
}
if (!name_valid) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
s_name_buf[0] = '\0';
ImGui::CloseCurrentPopup();
}
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;
}
} // namespace fn_ui
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
namespace fn_ui {
struct LayoutCallbacks {
// Lista de nombres de layouts guardados. Se llama solo cuando el menu
// Layouts esta abierto, por lo que puede consultar la BD on demand.
std::function<std::vector<std::string>()> list;
// Usuario clico un layout para aplicarlo. La app debe diferir el
// ImGui::LoadIniSettingsFromMemory hasta el inicio del proximo frame.
std::function<void(const std::string& name)> on_apply;
// Usuario guardo el layout actual con ese nombre.
std::function<void(const std::string& name)> on_save;
// Usuario borro un layout.
std::function<void(const std::string& name)> on_delete;
// Usuario clico "Reset to default".
std::function<void()> on_reset;
// Nombre del layout activo (para mostrar marker visual). Vacio = ninguno.
std::string active_name;
};
// Dibuja BeginMenu(menu_label) ... EndMenu() con:
// * Lista de layouts guardados (cb.list()), cada uno como MenuItem clickeable.
// Un marker "* " prefija el nombre si coincide con cb.active_name.
// * Separator
// * "Save current as..." (abre popup con InputText)
// * "Delete" (submenu listando los layouts; click = on_delete)
// * Separator
// * "Reset to default"
//
// Llamar solo dentro de un BeginMainMenuBar() exitoso.
// Si algun callback es nullptr, se salta esa accion silenciosamente.
// Returns: true si el usuario disparo alguna accion (apply/save/delete/reset).
bool layouts_menu_items(const char* menu_label, LayoutCallbacks& cb);
} // namespace fn_ui
+117
View File
@@ -0,0 +1,117 @@
---
name: layouts_menu
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool fn_ui::layouts_menu_items(const char* menu_label, fn_ui::LayoutCallbacks& cb)"
description: "Menu ImGui de gestion de layouts de ventana (guardar, aplicar, borrar, reset). Se dibuja como BeginMenu..EndMenu para componer dentro de una MainMenuBar propia. Toda la persistencia se delega en callbacks."
tags: [imgui, ui, menu, layouts, dockspace, persistence]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/layouts_menu.cpp"
framework: imgui
params:
- name: menu_label
desc: "Texto del menu desplegable, p.ej. 'Layouts'."
- name: cb
desc: "LayoutCallbacks con los cinco hooks (list, on_apply, on_save, on_delete, on_reset) y el campo active_name para marcar el layout activo. Callbacks nulos se saltan silenciosamente."
output: "true si el usuario disparo alguna accion (aplicar layout, guardar, borrar o reset) en este frame."
---
# layouts_menu
Renderiza un menu desplegable `Layouts` dentro de una `MainMenuBar` existente. Toda la logica de persistencia se delega en el struct `LayoutCallbacks` — la funcion solo gestiona la UI.
## Estructura del menu
```
Layouts
* mi_layout <- activo (marker "* ")
otro_layout <- inactivo (espacio " ")
─────────────────
Save current as... <- abre popup con InputText
Delete > <- submenu con todos los layouts
─────────────────
Reset to default
```
## Ejemplo de uso completo
```cpp
#include "core/layouts_menu.h"
#include <imgui.h>
#include <sqlite3.h>
#include "core/layout_storage_sqlite.h"
// Estado de la app
static std::string g_active_layout;
static std::string g_pending_blob; // diferir LoadIni al inicio del frame
// Construir callbacks cableadas contra SQLite
fn_ui::LayoutCallbacks make_layout_callbacks(sqlite3* db) {
fn_ui::LayoutCallbacks cb;
cb.list = [db]() -> std::vector<std::string> {
return fn_ui::layout_storage_list(db);
};
cb.on_apply = [db](const std::string& name) {
std::string blob = fn_ui::layout_storage_load_blob(db, name);
if (!blob.empty()) {
g_pending_blob = blob;
g_active_layout = name;
}
};
cb.on_save = [db](const std::string& name) {
std::size_t sz = 0;
const char* ini = ImGui::SaveIniSettingsToMemory(&sz);
fn_ui::layout_storage_save(db, name, std::string(ini, sz));
g_active_layout = name;
};
cb.on_delete = [db](const std::string& name) {
fn_ui::layout_storage_delete(db, name);
if (g_active_layout == name) g_active_layout.clear();
};
cb.on_reset = []() {
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::MarkIniSettingsDirty();
g_active_layout.clear();
};
return cb;
}
// Dentro del loop de render (inicio de frame):
if (!g_pending_blob.empty()) {
ImGui::LoadIniSettingsFromMemory(g_pending_blob.c_str(), g_pending_blob.size());
g_pending_blob.clear();
}
// Usar dentro de BeginMainMenuBar:
auto cb = make_layout_callbacks(db);
cb.active_name = g_active_layout;
if (ImGui::BeginMainMenuBar()) {
fn_ui::layouts_menu_items("Layouts", cb);
ImGui::EndMainMenuBar();
}
```
## Notas
- El popup "Save current as..." usa un buffer estatico de 64 chars. El boton Save esta deshabilitado mientras el nombre este vacio.
- El submenu Delete muestra `(no layouts)` si la lista esta vacia (item deshabilitado).
- `cb.active_name` debe setearse cada frame antes de llamar la funcion; la funcion no guarda estado entre frames.
- `ImGui::LoadIniSettingsFromMemory` debe llamarse al inicio del frame siguiente al `on_apply`, no desde dentro del callback (puede corromper el estado de ImGui si se llama a mitad de un frame de render). Usar un string `g_pending_blob` como se muestra en el ejemplo.
+33
View File
@@ -0,0 +1,33 @@
#include "core/panel_menu.h"
#include <imgui.h>
namespace fn_ui {
bool panel_menu_items(const char* menu_label,
const PanelToggle* items, std::size_t count) {
if (!items) count = 0;
bool changed = false;
if (ImGui::BeginMenu(menu_label)) {
for (std::size_t i = 0; i < count; ++i) {
const PanelToggle& item = items[i];
if (!item.open) continue;
bool clicked = ImGui::MenuItem(item.label, item.shortcut, item.open);
changed |= clicked;
}
ImGui::EndMenu();
}
return changed;
}
bool panel_menu(const char* menu_label,
const PanelToggle* items, std::size_t count) {
if (!ImGui::BeginMainMenuBar()) return false;
bool changed = panel_menu_items(menu_label, items, count);
ImGui::EndMainMenuBar();
return changed;
}
} // namespace fn_ui
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstddef>
namespace fn_ui {
struct PanelToggle {
const char* label; // texto del MenuItem y title del panel
const char* shortcut; // texto cosmetico tipo "Ctrl+1"; nullptr = sin shortcut
bool* open; // bool* del Begin/End del panel (no debe ser nullptr)
};
// Renderiza una MainMenuBar completa con un BeginMenu(menu_label) que lista
// cada panel como ImGui::MenuItem checkable. Toggle en el menu sincroniza
// el bool del PanelToggle (mismo bool* que el caller pasa a ImGui::Begin).
//
// - menu_label: texto del menu, p.ej. "View"
// - items: puntero a array de PanelToggle (no debe ser nullptr si count > 0)
// - count: numero de items
//
// Returns: true si el usuario togglo algun panel en este frame.
//
// Nota: la funcion abre y cierra la MainMenuBar internamente. El caller
// no debe envolver con BeginMainMenuBar.
bool panel_menu(const char* menu_label,
const PanelToggle* items, std::size_t count);
// Dibuja solo BeginMenu(menu_label) ... EndMenu() con los toggles, SIN
// abrir/cerrar BeginMainMenuBar. Usar esto cuando se quieran componer
// varios menus dentro de una MainMenuBar propia (ver app_menubar).
// Debe llamarse solo si ya se esta dentro de un BeginMainMenuBar() exitoso.
//
// Returns: true si el usuario togglo algun panel en este frame.
bool panel_menu_items(const char* menu_label,
const PanelToggle* items, std::size_t count);
} // namespace fn_ui
+76
View File
@@ -0,0 +1,76 @@
---
name: panel_menu
kind: component
lang: cpp
domain: core
version: "1.1.0"
purity: pure
signature: "bool fn_ui::panel_menu(const char* menu_label, const fn_ui::PanelToggle* items, size_t count); bool fn_ui::panel_menu_items(const char* menu_label, const fn_ui::PanelToggle* items, size_t count)"
description: "MainMenuBar ImGui con un menu checkable para abrir/cerrar paneles. panel_menu abre y cierra la MainMenuBar internamente. panel_menu_items dibuja solo el BeginMenu..EndMenu para componer con otras entradas dentro de una MainMenuBar propia."
tags: [imgui, ui, menu, panels, layout, dockspace]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/panel_menu.cpp"
framework: imgui
params:
- name: menu_label
desc: "Texto del menu desplegable, p.ej. 'View'."
- name: items
desc: "Puntero a array de PanelToggle. Cada toggle expone un bool* que controla la visibilidad del panel correspondiente."
- name: count
desc: "Numero de elementos en items."
output: "true si el usuario togglo algun panel este frame; false si no hubo cambios o si la MainMenuBar no se abrio (solo para panel_menu)."
---
# panel_menu
Renderiza una `ImGui::BeginMainMenuBar` completa con un unico menu desplegable (`menu_label`) que lista cada panel registrado como `ImGui::MenuItem` checkable. El `bool*` de cada `PanelToggle` es el mismo puntero que el caller pasa a `ImGui::Begin("Nombre", &show_panel)`, de modo que la X de la ventana y el item del menu se mantienen sincronizados automaticamente — ambos escriben en el mismo bool.
## Dos variantes (v1.1.0)
- **`panel_menu`**: abre y cierra la `MainMenuBar` internamente. Conveniente para apps con un solo menu.
- **`panel_menu_items`**: dibuja solo `BeginMenu` ... `EndMenu` sin tocar la `MainMenuBar`. Llamar dentro de un `BeginMainMenuBar()` exitoso para componer con otros menus (ej. con `layouts_menu_items` via `app_menubar`).
## Ejemplo de uso — panel_menu (standalone)
```cpp
#include "core/panel_menu.h"
#include <imgui.h>
// En la clase/estado de la app:
static bool show_code = true;
static bool show_preview = true;
static bool show_dag = true;
// Dentro del loop de render:
fn_ui::PanelToggle toggles[] = {
{ "Code Editor", "Ctrl+1", &show_code },
{ "Preview", "Ctrl+2", &show_preview },
{ "DAG", "Ctrl+3", &show_dag },
};
fn_ui::panel_menu("View", toggles, std::size(toggles));
```
## Ejemplo de uso — panel_menu_items (compuesto)
```cpp
if (ImGui::BeginMainMenuBar()) {
fn_ui::panel_menu_items("View", toggles, std::size(toggles));
// aqui van otros menus...
ImGui::EndMainMenuBar();
}
```
## Notas
- Si `items == nullptr` y `count > 0`, se trata como `count = 0` (no crashea).
- Si `item.open == nullptr` para algun item, ese item se salta silenciosamente.
- `shortcut` es puramente cosmetico — ImGui lo dibuja alineado a la derecha pero no registra ningun hotkey real.
- Compatible con dockspace: llamar antes del `DockSpaceOverViewport`.