feat(cpp/core): añadir layout_storage publico (SQLite-backed LayoutCallbacks)

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.
This commit is contained in:
2026-04-28 23:39:34 +02:00
parent 200e98e94c
commit c659120f86
4 changed files with 389 additions and 16 deletions
+18 -16
View File
@@ -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.
+187
View File
@@ -0,0 +1,187 @@
#include "core/layout_storage.h"
#include <imgui.h>
#include <sqlite3.h>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>
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<seconds>(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<std::string> layout_storage_list(LayoutStorage* s) {
std::vector<std::string> 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<const char*>(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<int>(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<const char*>(t),
static_cast<std::size_t>(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
+83
View File
@@ -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<std::string> 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
+101
View File
@@ -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<std::string> 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.