feat(cpp/core): app framework UI (settings, menubar, layouts)
5 primitivas que componen el chrome de una app fn_ui completa: - app_settings_cpp_core (function, impure) ventana flotante de settings globales (Display + Typography + secciones extra registrables) con persistencia automatica en app_settings.ini junto al ejecutable. - app_menubar_cpp_core (component, pure) MainMenuBar unificada con menu View (toggles de paneles) y Layouts. Punto de entrada de la menubar de cualquier app fn_ui. - layouts_menu_cpp_core (component, pure) menu para guardar/aplicar/ borrar/reset layouts de ImGui via callbacks (no I/O propio). - panel_menu_cpp_core (component, pure) menu checkable para abrir/cerrar paneles, composable dentro de la MainMenuBar. - layout_storage_sqlite_cpp_core (function, impure) primitivas CRUD de bajo nivel para persistir blobs INI de ImGui en sqlite (ui_layouts table). Permiten que apps como shaders_lab y registry_dashboard ofrezcan: panel toggles, layouts persistentes en BD, ventana de settings con preview en vivo. Todas usan tokens_cpp_core para el styling.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user