From 8d28faf3e8b045b5b6fcc46b81541bd0181f92b1 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:25:51 +0200 Subject: [PATCH] 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. --- cpp/functions/core/app_menubar.cpp | 30 +++ cpp/functions/core/app_menubar.h | 25 +++ cpp/functions/core/app_menubar.md | 126 +++++++++++ cpp/functions/core/app_settings.cpp | 221 +++++++++++++++++++ cpp/functions/core/app_settings.h | 87 ++++++++ cpp/functions/core/app_settings.md | 114 ++++++++++ cpp/functions/core/layout_storage_sqlite.cpp | 132 +++++++++++ cpp/functions/core/layout_storage_sqlite.h | 37 ++++ cpp/functions/core/layout_storage_sqlite.md | 110 +++++++++ cpp/functions/core/layouts_menu.cpp | 93 ++++++++ cpp/functions/core/layouts_menu.h | 45 ++++ cpp/functions/core/layouts_menu.md | 117 ++++++++++ cpp/functions/core/panel_menu.cpp | 33 +++ cpp/functions/core/panel_menu.h | 36 +++ cpp/functions/core/panel_menu.md | 76 +++++++ 15 files changed, 1282 insertions(+) create mode 100644 cpp/functions/core/app_menubar.cpp create mode 100644 cpp/functions/core/app_menubar.h create mode 100644 cpp/functions/core/app_menubar.md create mode 100644 cpp/functions/core/app_settings.cpp create mode 100644 cpp/functions/core/app_settings.h create mode 100644 cpp/functions/core/app_settings.md create mode 100644 cpp/functions/core/layout_storage_sqlite.cpp create mode 100644 cpp/functions/core/layout_storage_sqlite.h create mode 100644 cpp/functions/core/layout_storage_sqlite.md create mode 100644 cpp/functions/core/layouts_menu.cpp create mode 100644 cpp/functions/core/layouts_menu.h create mode 100644 cpp/functions/core/layouts_menu.md create mode 100644 cpp/functions/core/panel_menu.cpp create mode 100644 cpp/functions/core/panel_menu.h create mode 100644 cpp/functions/core/panel_menu.md diff --git a/cpp/functions/core/app_menubar.cpp b/cpp/functions/core/app_menubar.cpp new file mode 100644 index 00000000..b853bb7e --- /dev/null +++ b/cpp/functions/core/app_menubar.cpp @@ -0,0 +1,30 @@ +#include "core/app_menubar.h" +#include + +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 diff --git a/cpp/functions/core/app_menubar.h b/cpp/functions/core/app_menubar.h new file mode 100644 index 00000000..daf0ae0c --- /dev/null +++ b/cpp/functions/core/app_menubar.h @@ -0,0 +1,25 @@ +#pragma once +#include +#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 diff --git a/cpp/functions/core/app_menubar.md b/cpp/functions/core/app_menubar.md new file mode 100644 index 00000000..153c8122 --- /dev/null +++ b/cpp/functions/core/app_menubar.md @@ -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 +#include + +// 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). diff --git a/cpp/functions/core/app_settings.cpp b/cpp/functions/core/app_settings.cpp new file mode 100644 index 00000000..fa28391a --- /dev/null +++ b/cpp/functions/core/app_settings.cpp @@ -0,0 +1,221 @@ +#include "app_settings.h" + +#include "imgui.h" + +#include +#include +#include +#include +#include + +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 render; +}; +std::vector 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(v); + } else if (key == "font_size_px") { + float v = static_cast(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(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 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(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 diff --git a/cpp/functions/core/app_settings.h b/cpp/functions/core/app_settings.h new file mode 100644 index 00000000..51628845 --- /dev/null +++ b/cpp/functions/core/app_settings.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +// 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 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 diff --git a/cpp/functions/core/app_settings.md b/cpp/functions/core/app_settings.md new file mode 100644 index 00000000..bcec249e --- /dev/null +++ b/cpp/functions/core/app_settings.md @@ -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 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 `/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. **** (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 + +`/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//`). 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). diff --git a/cpp/functions/core/layout_storage_sqlite.cpp b/cpp/functions/core/layout_storage_sqlite.cpp new file mode 100644 index 00000000..73b1af6b --- /dev/null +++ b/cpp/functions/core/layout_storage_sqlite.cpp @@ -0,0 +1,132 @@ +#include "core/layout_storage_sqlite.h" +#include +#include +#include +#include +#include + +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 layout_storage_list(sqlite3* db) { + std::vector 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(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(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 diff --git a/cpp/functions/core/layout_storage_sqlite.h b/cpp/functions/core/layout_storage_sqlite.h new file mode 100644 index 00000000..8e007c6e --- /dev/null +++ b/cpp/functions/core/layout_storage_sqlite.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include + +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 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 diff --git a/cpp/functions/core/layout_storage_sqlite.md b/cpp/functions/core/layout_storage_sqlite.md new file mode 100644 index 00000000..89de5b4f --- /dev/null +++ b/cpp/functions/core/layout_storage_sqlite.md @@ -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 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 +#include + +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. diff --git a/cpp/functions/core/layouts_menu.cpp b/cpp/functions/core/layouts_menu.cpp new file mode 100644 index 00000000..304973f7 --- /dev/null +++ b/cpp/functions/core/layouts_menu.cpp @@ -0,0 +1,93 @@ +#include "core/layouts_menu.h" +#include +#include + +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 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 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 diff --git a/cpp/functions/core/layouts_menu.h b/cpp/functions/core/layouts_menu.h new file mode 100644 index 00000000..702f2908 --- /dev/null +++ b/cpp/functions/core/layouts_menu.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include + +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()> list; + + // Usuario clico un layout para aplicarlo. La app debe diferir el + // ImGui::LoadIniSettingsFromMemory hasta el inicio del proximo frame. + std::function on_apply; + + // Usuario guardo el layout actual con ese nombre. + std::function on_save; + + // Usuario borro un layout. + std::function on_delete; + + // Usuario clico "Reset to default". + std::function 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 diff --git a/cpp/functions/core/layouts_menu.md b/cpp/functions/core/layouts_menu.md new file mode 100644 index 00000000..5fbb1f8b --- /dev/null +++ b/cpp/functions/core/layouts_menu.md @@ -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 +#include +#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 { + 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. diff --git a/cpp/functions/core/panel_menu.cpp b/cpp/functions/core/panel_menu.cpp new file mode 100644 index 00000000..342a8da0 --- /dev/null +++ b/cpp/functions/core/panel_menu.cpp @@ -0,0 +1,33 @@ +#include "core/panel_menu.h" +#include + +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 diff --git a/cpp/functions/core/panel_menu.h b/cpp/functions/core/panel_menu.h new file mode 100644 index 00000000..4d18f540 --- /dev/null +++ b/cpp/functions/core/panel_menu.h @@ -0,0 +1,36 @@ +#pragma once +#include + +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 diff --git a/cpp/functions/core/panel_menu.md b/cpp/functions/core/panel_menu.md new file mode 100644 index 00000000..ca59de2c --- /dev/null +++ b/cpp/functions/core/panel_menu.md @@ -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 + +// 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`.