From c659120f866f61acfe7e10243c55eed724d55242 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 28 Apr 2026 23:39:34 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(cpp/core):=20a=C3=B1adir=20layout=5Fst?= =?UTF-8?q?orage=20publico=20(SQLite-backed=20LayoutCallbacks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API publica con handle opaco LayoutStorage* que envuelve la persistencia de layouts ImGui en SQLite. Cualquier app puede obtener un LayoutCallbacks listo para app_menubar/layouts_menu_items con dos llamadas: auto* st = fn_ui::layout_storage_open("app.db"); fn_ui::LayoutCallbacks cb; fn_ui::layout_storage_make_callbacks(st, cb); Tabla SQLite imgui_layouts(name, ini, updated_at) creada con CREATE TABLE IF NOT EXISTS para no chocar con tablas pre-existentes. fn_framework ahora enlaza SQLite::SQLite3 para que cualquier app que use el framework herede acceso a layout_storage sin trabajo extra. --- cpp/CMakeLists.txt | 34 ++--- cpp/functions/core/layout_storage.cpp | 187 ++++++++++++++++++++++++++ cpp/functions/core/layout_storage.h | 83 ++++++++++++ cpp/functions/core/layout_storage.md | 101 ++++++++++++++ 4 files changed, 389 insertions(+), 16 deletions(-) create mode 100644 cpp/functions/core/layout_storage.cpp create mode 100644 cpp/functions/core/layout_storage.h create mode 100644 cpp/functions/core/layout_storage.md diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 749aab33..64f57533 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -91,6 +91,22 @@ endif() target_link_libraries(imgui PUBLIC ${PLATFORM_LIBS}) +# --- SQLite3 (shared by every app that uses it, including fn_framework for +# layout_storage) --- +# System on Linux, vendored amalgamation on Windows cross-compile. +find_package(SQLite3 QUIET) +if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored) + set(SQLITE3_AMALG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3) + add_library(sqlite3_vendored STATIC ${SQLITE3_AMALG_DIR}/sqlite3.c) + target_include_directories(sqlite3_vendored PUBLIC ${SQLITE3_AMALG_DIR}) + target_compile_definitions(sqlite3_vendored PRIVATE + SQLITE_THREADSAFE=1 + SQLITE_ENABLE_FTS5 + SQLITE_ENABLE_JSON1 + ) + add_library(SQLite::SQLite3 ALIAS sqlite3_vendored) +endif() + # --- Framework --- # Incluye tokens.cpp (identidad visual Mantine dark + indigo), icon_font.cpp # (Karla/Roboto/... + Tabler), app_settings.cpp (persistencia y ventana de @@ -105,6 +121,7 @@ add_library(fn_framework STATIC functions/core/panel_menu.cpp functions/core/layouts_menu.cpp functions/core/app_menubar.cpp + functions/core/layout_storage.cpp ) target_include_directories(fn_framework PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/framework @@ -115,7 +132,7 @@ target_include_directories(fn_framework PUBLIC target_compile_definitions(fn_framework PUBLIC FN_CPP_ROOT="${CMAKE_CURRENT_SOURCE_DIR}" ) -target_link_libraries(fn_framework PUBLIC imgui implot implot3d) +target_link_libraries(fn_framework PUBLIC imgui implot implot3d SQLite::SQLite3) if(TRACY_ENABLE) target_link_libraries(fn_framework PUBLIC tracy) endif() @@ -154,21 +171,6 @@ function(add_imgui_app target) ) endfunction() -# --- SQLite3 (shared by every app that uses it) --- -# System on Linux, vendored amalgamation on Windows cross-compile. -find_package(SQLite3 QUIET) -if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored) - set(SQLITE3_AMALG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3) - add_library(sqlite3_vendored STATIC ${SQLITE3_AMALG_DIR}/sqlite3.c) - target_include_directories(sqlite3_vendored PUBLIC ${SQLITE3_AMALG_DIR}) - target_compile_definitions(sqlite3_vendored PRIVATE - SQLITE_THREADSAFE=1 - SQLITE_ENABLE_FTS5 - SQLITE_ENABLE_JSON1 - ) - add_library(SQLite::SQLite3 ALIAS sqlite3_vendored) -endif() - # --- Function libraries (headers for composition) --- # Functions are compiled as part of apps that use them via add_imgui_app. # Each function is a .h/.cpp pair included by the app's CMakeLists.txt. diff --git a/cpp/functions/core/layout_storage.cpp b/cpp/functions/core/layout_storage.cpp new file mode 100644 index 00000000..9cb52878 --- /dev/null +++ b/cpp/functions/core/layout_storage.cpp @@ -0,0 +1,187 @@ +#include "core/layout_storage.h" + +#include +#include + +#include +#include +#include +#include + +namespace fn_ui { + +// ── handle interno ──────────────────────────────────────────────────────── + +struct LayoutStorage { + sqlite3* db = nullptr; + std::string pending_blob; // INI a aplicar al inicio del proximo frame + std::string pending_name; // nombre del layout pendiente +}; + +// ── helpers ─────────────────────────────────────────────────────────────── + +namespace { + +int64_t now_unix() { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + +bool ensure_schema(sqlite3* db) { + // CREATE TABLE IF NOT EXISTS — no toca tablas pre-existentes con otro + // schema (ej. ui_layouts heredada de layout_storage_sqlite). + const char* sql = + "CREATE TABLE IF NOT EXISTS imgui_layouts (" + " name TEXT PRIMARY KEY," + " ini TEXT NOT NULL," + " updated_at INTEGER NOT NULL" + ");"; + char* errmsg = nullptr; + int rc = sqlite3_exec(db, sql, nullptr, nullptr, &errmsg); + if (errmsg) sqlite3_free(errmsg); + return rc == SQLITE_OK; +} + +} // namespace + +// ── API ─────────────────────────────────────────────────────────────────── + +LayoutStorage* layout_storage_open(const char* db_path) { + if (!db_path || !*db_path) return nullptr; + sqlite3* db = nullptr; + int rc = sqlite3_open(db_path, &db); + if (rc != SQLITE_OK || !db) { + if (db) sqlite3_close(db); + return nullptr; + } + if (!ensure_schema(db)) { + sqlite3_close(db); + return nullptr; + } + auto* s = new LayoutStorage{}; + s->db = db; + return s; +} + +void layout_storage_close(LayoutStorage* s) { + if (!s) return; + if (s->db) sqlite3_close(s->db); + delete s; +} + +std::vector layout_storage_list(LayoutStorage* s) { + std::vector out; + if (!s || !s->db) return out; + const char* sql = + "SELECT name FROM imgui_layouts ORDER BY updated_at DESC;"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, nullptr) != SQLITE_OK) + return out; + while (sqlite3_step(stmt) == SQLITE_ROW) { + const unsigned char* t = sqlite3_column_text(stmt, 0); + if (t) out.emplace_back(reinterpret_cast(t)); + } + sqlite3_finalize(stmt); + return out; +} + +bool layout_storage_save(LayoutStorage* s, const std::string& name) { + if (!s || !s->db || name.empty()) return false; + + std::size_t sz = 0; + const char* ini = ImGui::SaveIniSettingsToMemory(&sz); + if (!ini || sz == 0) return false; + + const char* sql = + "INSERT INTO imgui_layouts (name, ini, updated_at) " + "VALUES (?, ?, ?) " + "ON CONFLICT(name) DO UPDATE SET " + " ini = excluded.ini, " + " updated_at = excluded.updated_at;"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(s->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, ini, static_cast(sz), SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 3, now_unix()); + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE; +} + +bool layout_storage_apply(LayoutStorage* s, const std::string& name) { + if (!s || !s->db || name.empty()) return false; + const char* sql = "SELECT ini FROM imgui_layouts WHERE name = ?;"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, nullptr) != SQLITE_OK) + return false; + sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT); + bool found = false; + if (sqlite3_step(stmt) == SQLITE_ROW) { + const unsigned char* t = sqlite3_column_text(stmt, 0); + int len = sqlite3_column_bytes(stmt, 0); + if (t && len > 0) { + s->pending_blob.assign(reinterpret_cast(t), + static_cast(len)); + s->pending_name = name; + found = true; + } + } + sqlite3_finalize(stmt); + return found; +} + +std::string layout_storage_apply_pending(LayoutStorage* s) { + if (!s || s->pending_blob.empty()) return ""; + ImGui::LoadIniSettingsFromMemory(s->pending_blob.data(), + s->pending_blob.size()); + std::string applied = std::move(s->pending_name); + s->pending_blob.clear(); + s->pending_name.clear(); + return applied; +} + +bool layout_storage_delete(LayoutStorage* s, const std::string& name) { + if (!s || !s->db || name.empty()) return false; + const char* sql = "DELETE FROM imgui_layouts WHERE name = ?;"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(s->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(s->db); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE && changes > 0; +} + +void layout_storage_make_callbacks(LayoutStorage* s, LayoutCallbacks& out) { + out.list = [s]() { + return layout_storage_list(s); + }; + out.on_apply = [s, &out](const std::string& name) { + if (layout_storage_apply(s, name)) { + // active_name se ajustara cuando layout_storage_apply_pending + // confirme que se aplico (la app debe tomar el valor de retorno + // y propagarlo). Aqui ya lo dejamos optimistamente. + out.active_name = name; + } + }; + out.on_save = [s, &out](const std::string& name) { + if (layout_storage_save(s, name)) { + out.active_name = name; + } + }; + out.on_delete = [s, &out](const std::string& name) { + layout_storage_delete(s, name); + if (out.active_name == name) out.active_name.clear(); + }; + out.on_reset = [&out]() { + // Limpia el INI en memoria. La app puede ademas re-mostrar paneles. + ImGui::LoadIniSettingsFromMemory("", 0); + ImGuiIO& io = ImGui::GetIO(); + io.WantSaveIniSettings = true; + out.active_name.clear(); + }; +} + +} // namespace fn_ui diff --git a/cpp/functions/core/layout_storage.h b/cpp/functions/core/layout_storage.h new file mode 100644 index 00000000..d72d40bb --- /dev/null +++ b/cpp/functions/core/layout_storage.h @@ -0,0 +1,83 @@ +#pragma once +// +// layout_storage — API publica de persistencia de layouts ImGui en SQLite. +// +// Ofrece un handle opaco (LayoutStorage*) para que las apps no tengan que +// gestionar sqlite3 ni el cableado save/load/list/delete contra el menu de +// layouts. Una app obtiene un LayoutCallbacks listo para pasarse a +// app_menubar() / layouts_menu_items() con dos llamadas: +// +// auto* st = fn_ui::layout_storage_open("my_app.db"); +// fn_ui::LayoutCallbacks cb; +// fn_ui::layout_storage_make_callbacks(st, cb); +// // ... +// fn_ui::layout_storage_close(st); +// +// Internamente: +// * Tabla SQLite "imgui_layouts(name TEXT PRIMARY KEY, ini TEXT NOT NULL, +// updated_at INTEGER NOT NULL)". +// * save: serializa ImGui::SaveIniSettingsToMemory() y hace UPSERT por +// nombre. +// * load (on_apply): NO llama a ImGui::LoadIniSettingsFromMemory directo; +// guarda el blob pendiente y lo aplica al inicio del proximo frame +// (ver layout_storage_apply_pending). +// * list: SELECT name FROM imgui_layouts ORDER BY updated_at DESC. +// * remove: DELETE FROM imgui_layouts WHERE name = ?. +// +// La API es impure: hace I/O contra disco. Nunca lanza excepciones; en error +// las operaciones devuelven false / vector vacio / string vacia. + +#include "core/layouts_menu.h" // for fn_ui::LayoutCallbacks + +namespace fn_ui { + +struct LayoutStorage; // opaco — definido en layout_storage.cpp + +// Abre (o crea) la BD en `db_path` y garantiza la tabla `imgui_layouts`. +// Retorna nullptr si no se puede abrir el fichero. +// El caller posee el handle y debe cerrarlo con layout_storage_close. +LayoutStorage* layout_storage_open(const char* db_path); + +// Cierra la BD y libera el handle. Seguro con nullptr (no-op). +void layout_storage_close(LayoutStorage* s); + +// Lista nombres de layouts guardados, ordenados por updated_at DESC (mas +// recientes primero). Retorna vector vacio en error o si s == nullptr. +std::vector layout_storage_list(LayoutStorage* s); + +// Guarda (UPSERT) un layout serializando ImGui::SaveIniSettingsToMemory(). +// Debe llamarse desde un contexto con ImGui inicializado. +// Retorna true si la fila se inserto/actualizo, false en error. +bool layout_storage_save(LayoutStorage* s, const std::string& name); + +// Marca un layout como "pendiente de aplicar". Carga el blob INI desde la BD +// pero NO llama a ImGui::LoadIniSettingsFromMemory todavia: aplicar settings +// dentro de un frame es inseguro, asi que se aplica en el proximo +// layout_storage_apply_pending. +// Retorna true si encontro el layout y quedo pendiente. +bool layout_storage_apply(LayoutStorage* s, const std::string& name); + +// Aplica el blob pendiente (si lo hay) llamando a +// ImGui::LoadIniSettingsFromMemory. Llamar al INICIO del frame, antes de +// crear ventanas. No-op si no hay nada pendiente. +// Retorna el nombre del layout que se acaba de aplicar (vacio si no se +// aplico nada). Util para que la app actualice su `active_name`. +std::string layout_storage_apply_pending(LayoutStorage* s); + +// Borra un layout por nombre. Retorna true si se borro al menos una fila. +bool layout_storage_delete(LayoutStorage* s, const std::string& name); + +// Rellena `out` con callbacks que envuelven este storage. Patron de uso: +// +// auto* st = fn_ui::layout_storage_open("app.db"); +// fn_ui::LayoutCallbacks cb; +// fn_ui::layout_storage_make_callbacks(st, cb); +// // pasar cb a app_menubar() o layouts_menu_items() en cada frame +// // y llamar fn_ui::layout_storage_apply_pending(st) al inicio del frame +// +// El handle `s` debe permanecer vivo durante toda la vida de los callbacks. +// `out.active_name` se actualiza automaticamente en on_save/on_apply/on_reset +// /on_delete. +void layout_storage_make_callbacks(LayoutStorage* s, LayoutCallbacks& out); + +} // namespace fn_ui diff --git a/cpp/functions/core/layout_storage.md b/cpp/functions/core/layout_storage.md new file mode 100644 index 00000000..bcce0868 --- /dev/null +++ b/cpp/functions/core/layout_storage.md @@ -0,0 +1,101 @@ +--- +name: layout_storage +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: impure +signature: "fn_ui::LayoutStorage* layout_storage_open(const char*); void layout_storage_close(LayoutStorage*); std::vector layout_storage_list(LayoutStorage*); bool layout_storage_save(LayoutStorage*, const std::string&); bool layout_storage_apply(LayoutStorage*, const std::string&); std::string layout_storage_apply_pending(LayoutStorage*); bool layout_storage_delete(LayoutStorage*, const std::string&); void layout_storage_make_callbacks(LayoutStorage*, LayoutCallbacks&)" +description: "Persistencia de layouts ImGui en SQLite con handle opaco. Una app abre el storage con un path, obtiene un LayoutCallbacks listo para pasar al menu de layouts (app_menubar/layouts_menu_items) y solo necesita llamar a layout_storage_apply_pending() al inicio de cada frame para activar layouts cargados." +tags: [imgui, sqlite, layouts, persistence, dockspace, public-api] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [sqlite3, imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/layout_storage.cpp" +params: + - name: db_path + desc: "Ruta a la BD SQLite. Si no existe, se crea. Acepta ':memory:'." + - name: s + desc: "Handle opaco LayoutStorage* obtenido con layout_storage_open." + - name: name + desc: "Nombre del layout (clave primaria en imgui_layouts)." + - name: out + desc: "LayoutCallbacks a rellenar con los handlers que envuelven el storage." +output: "open retorna LayoutStorage* o nullptr si falla. list retorna nombres ordenados por updated_at DESC. save/apply/delete retornan bool de exito. apply_pending retorna el nombre del layout aplicado (vacio si no habia pendiente). make_callbacks no retorna nada y deja `out` listo para pasar a layouts_menu_items." +--- + +# layout_storage + +API publica de persistencia de layouts ImGui en SQLite. Reemplaza al patron manual donde cada app gestionaba su propia conexion `sqlite3*` y cableaba los callbacks contra `layout_storage_sqlite` (que sigue disponible como capa de bajo nivel). + +## Schema + +```sql +CREATE TABLE IF NOT EXISTS imgui_layouts ( + name TEXT PRIMARY KEY, + ini TEXT NOT NULL, + updated_at INTEGER NOT NULL -- unix epoch en segundos +); +``` + +`CREATE TABLE IF NOT EXISTS` significa que la nueva API convive con tablas pre-existentes en la misma BD (por ejemplo `ui_layouts` heredada o cualquier otra). + +## Patron de uso (recomendado) + +```cpp +#include "core/layout_storage.h" +#include "core/app_menubar.h" + +static fn_ui::LayoutStorage* g_layouts = nullptr; +static fn_ui::LayoutCallbacks g_layout_cb; + +void app_init() { + g_layouts = fn_ui::layout_storage_open("my_app.db"); + fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb); +} + +void on_frame() { + // 1) Aplicar layout pendiente al INICIO del frame (antes de NewFrame + // no es estrictamente necesario; ImGui acepta LoadIniSettingsFromMemory + // durante el frame, pero hacerlo aqui evita "saltos" visuales). + std::string applied = fn_ui::layout_storage_apply_pending(g_layouts); + if (!applied.empty()) g_layout_cb.active_name = applied; + + // 2) Renderizar la menubar pasando los callbacks. on_save/on_apply/etc + // ya estan cableados internamente. + fn::app_menubar(/* ..., */ &g_layout_cb /* , ... */); + + // 3) Tu UI normal. +} + +void app_shutdown() { + fn_ui::layout_storage_close(g_layouts); +} +``` + +## Flujo del save/load + +- **Save**: `on_save(name)` llama internamente a `ImGui::SaveIniSettingsToMemory()` y hace UPSERT por nombre. El blob es el formato INI nativo de ImGui (windows, dock layout, columns, etc.). +- **Apply**: `on_apply(name)` lee el blob de la BD pero NO llama a `LoadIniSettingsFromMemory` directamente. Lo deja "pendiente" para que `layout_storage_apply_pending()` lo aplique al inicio del proximo frame, fuera de cualquier render. Esto evita inconsistencias si el usuario cambia de layout en mitad de un frame. +- **Delete**: borra la fila por nombre. Si era el layout activo, `active_name` se limpia. +- **Reset**: limpia el INI en memoria con `LoadIniSettingsFromMemory("", 0)` y marca dirty para que ImGui regenere su layout default. + +## Errores + +Ninguna funcion lanza excepciones. En error: +- `open` retorna `nullptr`. +- `save`, `apply`, `delete` retornan `false`. +- `list` retorna vector vacio. +- `apply_pending` retorna string vacia. + +Pasar `nullptr` como handle es siempre seguro (no-op). + +## Relacion con `layout_storage_sqlite` + +Esta API es la capa publica recomendada para apps. `layout_storage_sqlite` sigue disponible como CRUD de bajo nivel para casos que necesiten compartir una conexion sqlite3 con otras tablas (por ejemplo `shaders_lab` que tambien tiene `shaderlab_db`). Las dos APIs usan tablas distintas (`imgui_layouts` vs `ui_layouts`) y pueden coexistir en la misma BD. From 7dc5b517268649144f087f4da0da4abf0a3843d9 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 28 Apr 2026 23:41:03 +0200 Subject: [PATCH 2/3] refactor(shaders_lab): migrar layouts inline a layout_storage publico MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sustituye ~30 lineas de cableado manual de save/load/list/delete contra layout_storage_sqlite por dos llamadas a la nueva API publica: g_layouts = fn_ui::layout_storage_open("shaders_lab.db"); fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb); El blob pendiente lo gestiona el storage (layout_storage_apply_pending). on_reset se override para ademas re-mostrar los paneles de shaders_lab. La tabla ui_layouts heredada queda intacta — la nueva API usa imgui_layouts en la misma BD. --- cpp/apps/shaders_lab/CMakeLists.txt | 4 +-- cpp/apps/shaders_lab/main.cpp | 54 +++++++++-------------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt index 49df45e1..b5da11af 100644 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -16,8 +16,8 @@ add_imgui_app(shaders_lab ${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_previews.cpp ${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp ${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp - # fps_overlay, panel_menu, layouts_menu, app_menubar ya viven en fn_framework - ${CMAKE_SOURCE_DIR}/functions/core/layout_storage_sqlite.cpp + # fps_overlay, panel_menu, layouts_menu, app_menubar, layout_storage ya + # viven en fn_framework. ) target_include_directories(shaders_lab PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/cpp/apps/shaders_lab/main.cpp b/cpp/apps/shaders_lab/main.cpp index fa7d0e5e..833eca56 100644 --- a/cpp/apps/shaders_lab/main.cpp +++ b/cpp/apps/shaders_lab/main.cpp @@ -17,7 +17,7 @@ #include "core/panel_menu.h" #include "core/layouts_menu.h" #include "core/app_menubar.h" -#include "core/layout_storage_sqlite.h" +#include "core/layout_storage.h" #include #include @@ -71,9 +71,10 @@ static bool g_show_functions = true; static bool g_show_generated = true; // ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ─────── +// El storage opaco encapsula la BD y el blob pendiente. Los callbacks +// envuelven save/apply/delete/reset y se pasan a app_menubar tal cual. +static fn_ui::LayoutStorage* g_layouts = nullptr; static fn_ui::LayoutCallbacks g_layout_cb; -static std::string g_pending_layout_blob; // applied at start of next frame -static std::string g_pending_layout_name; // becomes active_name after apply // ── Save-as-generator modal state ───────────────────────────────────────── static bool g_save_modal_open = false; @@ -222,13 +223,8 @@ static void load_user_generators_into_catalog() { static void render() { // Apply pending layout BEFORE any ImGui::Begin this frame. // (LoadIniSettingsFromMemory must happen before windows are submitted.) - if (!g_pending_layout_blob.empty()) { - ImGui::LoadIniSettingsFromMemory(g_pending_layout_blob.c_str(), - g_pending_layout_blob.size()); - g_layout_cb.active_name = g_pending_layout_name; - g_pending_layout_blob.clear(); - g_pending_layout_name.clear(); - } + std::string applied = fn_ui::layout_storage_apply_pending(g_layouts); + if (!applied.empty()) g_layout_cb.active_name = applied; if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code); if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag); @@ -412,37 +408,20 @@ int main() { load_user_generators_into_catalog(); ensure_dag_default(); - // Layout persistence on the same shaders_lab.db connection. - sqlite3* db = fn::gfx::shaderlab_db_handle(); - fn_ui::layout_storage_init(db); + // Layout persistence: handle opaco que crea su propia tabla + // imgui_layouts en shaders_lab.db (CREATE IF NOT EXISTS, no toca la + // tabla ui_layouts heredada). Cualquier app del registry puede usar + // este patron. + g_layouts = fn_ui::layout_storage_open("shaders_lab.db"); + fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb); - g_layout_cb.list = [db]() { - return fn_ui::layout_storage_list(db); - }; - g_layout_cb.on_apply = [db](const std::string& name) { - std::string blob = fn_ui::layout_storage_load_blob(db, name); - if (!blob.empty()) { - g_pending_layout_blob = std::move(blob); - g_pending_layout_name = name; - } - }; - g_layout_cb.on_save = [db](const std::string& name) { - size_t size = 0; - const char* blob = ImGui::SaveIniSettingsToMemory(&size); - if (blob && size > 0) { - fn_ui::layout_storage_save(db, name, std::string(blob, size)); - g_layout_cb.active_name = name; - } - }; - g_layout_cb.on_delete = [db](const std::string& name) { - fn_ui::layout_storage_delete(db, name); - if (g_layout_cb.active_name == name) g_layout_cb.active_name.clear(); - }; + // Override de on_reset: ademas de limpiar el INI, re-mostrar todos + // los paneles especificos de shaders_lab. g_layout_cb.on_reset = []() { - // Default reset: open every panel and clear active layout marker. - // The actual dock layout is whatever ImGui rebuilt on first launch. g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d = g_show_controls = g_show_functions = g_show_generated = true; + ImGui::LoadIniSettingsFromMemory("", 0); + ImGui::GetIO().WantSaveIniSettings = true; g_layout_cb.active_name.clear(); }; @@ -456,6 +435,7 @@ int main() { fn::gfx::canvas_destroy(g_canvas_dag); fn::gfx::dag_node_editor_destroy(); fn::gfx::dag_previews_destroy(); + fn_ui::layout_storage_close(g_layouts); fn::gfx::shaderlab_db_close(); return rc; } From cdba3869b2cf2b1d5512a3b4b641359b7ba0f528 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 28 Apr 2026 23:41:19 +0200 Subject: [PATCH 3/3] docs: cerrar issue 0042 --- .../0042-cpp-layout-storage-public.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 dev/issues/completed/0042-cpp-layout-storage-public.md diff --git a/dev/issues/completed/0042-cpp-layout-storage-public.md b/dev/issues/completed/0042-cpp-layout-storage-public.md new file mode 100644 index 00000000..1847f754 --- /dev/null +++ b/dev/issues/completed/0042-cpp-layout-storage-public.md @@ -0,0 +1,27 @@ +--- +id: "0042" +title: "C++ layout_storage: extraer y publicar como API reutilizable" +status: completed +created_at: 2026-04-28 +tags: [cpp, ui, refactor, layouts, sqlite] +--- + +# 0042 — C++ layout_storage: extraer y publicar como API reutilizable + +## Objetivo + +Extraer la persistencia de layouts ImGui (privada en `shaders_lab/main.cpp`) +a una funcion publica del registry: `layout_storage_cpp_core`. Cualquier app +puede pasarla a `app_menubar` via `LayoutCallbacks` con un solo setup. + +## Resultado + +- Nueva funcion publica `cpp/functions/core/layout_storage.{h,cpp,md}` con + handle opaco `LayoutStorage*` y helper `layout_storage_make_callbacks` que + rellena un `LayoutCallbacks` listo para `app_menubar`. +- Tabla SQLite `imgui_layouts(name, ini, updated_at)` creada con + `CREATE TABLE IF NOT EXISTS` para no chocar con tablas pre-existentes. +- `shaders_lab` migrado a la nueva API. La capa low-level + `layout_storage_sqlite` se deja intacta para casos que comparten conexion. +- `fn_framework` ahora enlaza `SQLite::SQLite3` para que cualquier app que + use el framework tenga acceso a `layout_storage` sin trabajo extra.