# cpp/PATTERNS.md — App shell canonico Patron obligatorio para apps C++ del registry (`cpp/apps/*`, `projects/*/apps/*`). Cumplir estas reglas garantiza coherencia visual, theming uniforme, About/Settings funcionando, paneles toggleables y layouts persistentes con cero codigo boilerplate. ## Checklist obligatorio Antes de mergear una app, verificar uno por uno: - [ ] **No `glfwInit` directo**. La app SOLO usa `fn::run_app(AppConfig{...}, render_fn)`. El framework gestiona GLFW + ImGui + ImPlot + theming + Settings + About + FPS overlay. - [ ] **About registrado**. La app pasa `AppConfig::about = {name, version, description}` o llama explicitamente `fn_ui::about_window_set_info(...)` en su init. - [ ] **Settings extras** (si aplica). Si la app expone settings propios (toggles, sliders, paths…), los registra con `fn_ui::settings_window_add_section("Mi App", cb)`. - [ ] **Paneles toggleables** (si aplica). Si la app tiene >=1 panel: ```cpp static constexpr fn_ui::PanelToggle panels[] = { {"Inspector", "Ctrl+1", &show_inspector}, {"Console", "Ctrl+2", &show_console}, }; ``` Pasarlo a `AppConfig::panels` + `AppConfig::panel_count = 2`. - [ ] **Layouts persistentes**. Vienen activos por defecto: `fn::run_app` abre un `LayoutStorage` SQLite en `/local_files/layouts.db` y enchufa el menu Layouts (Save / Apply / Delete / Reset) sin codigo. La app solo pasa `AppConfig::layouts_cb` si quiere personalizar (ej. on_reset que restaure paneles especificos como en `shaders_lab`). Para apagar el auto-storage: `cfg.auto_layouts = false`. Para cambiar el nombre del archivo: `cfg.auto_layouts_db = "myapp_layouts.db"`. - [ ] **GL loader** (si la app usa OpenGL >= 2.0 directamente). Pasar `AppConfig::init_gl_loader = true` para que `fn::run_app()` llame `fn::gfx::gl_loader_init()` tras crear el contexto. - [ ] **Tokens en lugar de hex literales**. Usar `fn_tokens::colors`, `fn_tokens::spacing`, `fn_tokens::radius`. Nunca `IM_COL32(0x12,0x34,...)`, nunca `ImVec4(0.5f, 0.5f, 0.5f, 1.0f)` ad-hoc. - [ ] **Componentes del registry, no raw ImGui con styling manual**. Evitar `ImGui::BeginTable / Selectable / BeginPopupModal / BeginChild` con estilos pegados a mano cuando ya existe primitiva: - `fn_ui::dashboard_grid` / `fn_ui::dashboard_panel` para layouts grid. - `fn_ui::tree_view` / `fn_ui::select` para listas seleccionables. - `fn_ui::modal_dialog` para popups modales. - [ ] **Iconos via `TI_*`** (Tabler). Nunca emojis ni hex UTF-8 inline. Ver `cpp/functions/core/icons_tabler.h`. - [ ] **Build incremental**. La app aparece en `cpp/CMakeLists.txt` con su `add_subdirectory(apps/)`. Sin warnings nuevos. ## Esqueleto minimo ```cpp #include "framework/app_base.h" #include "core/icons_tabler.h" #include "core/panel_menu.h" #include "core/app_settings.h" #include "core/tokens.h" #include "imgui.h" namespace { bool show_inspector = true; bool show_console = false; constexpr fn_ui::PanelToggle k_panels[] = { {"Inspector", "Ctrl+1", &show_inspector}, {"Console", "Ctrl+2", &show_console}, }; } // namespace static void render_my_app() { ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); if (show_inspector) { ImGui::Begin(TI_INFO_CIRCLE " Inspector", &show_inspector); ImGui::TextUnformatted("Inspector contents"); ImGui::End(); } if (show_console) { ImGui::Begin(TI_TERMINAL_2 " Console", &show_console); ImGui::TextUnformatted("Console contents"); ImGui::End(); } } int main() { fn::AppConfig cfg; cfg.title = "My App"; cfg.about = {"My App", "0.1.0", "Demo de app shell canonica"}; cfg.panels = k_panels; cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]); cfg.init_gl_loader = false; // ponerlo en true si usas OpenGL directo return fn::run_app(cfg, render_my_app); } ``` Con esto la app obtiene gratis: MainMenuBar (View/**Layouts**/Settings/About), ventana About, ventana Settings, ventana Logs, FPS overlay configurable, theming `FnDark`, fuentes vectoriales + iconos Tabler mergeados, multi-viewport opcional, **y persistencia de layouts ImGui en `/local_files/layouts.db`** sin escribir una linea de codigo. ## Anti-patrones | Mal patron | Patron correcto | |---------------------------------------|------------------------------------------------| | `glfwInit()` en `main` | `fn::run_app()` | | `ImVec4(0.5,0.5,0.5,1)` ad-hoc | `fn_tokens::colors::text_dim` | | Crear menubar a mano en cada frame | `AppConfig::panels` + `AppConfig::layouts_cb` | | `ImGui::Begin(u8"\xEF\xA0\x83 ...")` | `ImGui::Begin(TI_HOME " ...")` | | Settings dispersos por la app | `settings_window_add_section()` | | About hardcoded en un `Begin/End` | `AppConfig::about` o `about_window_set_info()` | | Llamar `gl*` sin loader en Windows | `AppConfig::init_gl_loader = true` | ## Cuando NO usar `fn::run_app` Solo si la app es: - un test headless que no necesita ventana (usar `googletest` directo); - un binario CLI sin UI (no es una "app C++" en este sentido). En cualquier otro caso, usar `fn::run_app`. Si `AppConfig` no expone algo que necesitas, **abrir un issue para extender el shell**, no duplicar boilerplate. ## Tests visuales y CI gate (issue 0048) `primitives_gallery` soporta un modo `--capture ` que renderiza cada demo en una ventana GLFW invisible y guarda un PNG por demo. Se usa para tests visuales tipo golden-image: ```bash # Regenerar goldens (cuando tu cambio es intencional): cpp/scripts/update_goldens.sh # Equivalente manual: LIBGL_ALWAYS_SOFTWARE=1 \ cpp/build/apps/primitives_gallery/primitives_gallery \ --capture cpp/tests/golden ``` `cpp/tests/test_visual.cpp` corre la captura sobre un tmpdir y compara contra `cpp/tests/golden/.png` con tolerancia 1% de pixels distintos (threshold 5/255 por canal). Skipea si: - `cpp/tests/golden/` esta vacio (no hay goldens todavia). - El binario `primitives_gallery` no se construyo. - El entorno no puede crear contexto GL (WSL minimo, container sin Mesa) — el test reporta SKIP en lugar de FAIL. Para diagnosticar un diff: revisar el PNG actual en `cpp/build/tests/visual_actual/.png` vs el golden en `cpp/tests/golden/.png`. ### CI gate `check_tested.sh` `cpp/scripts/check_tested.sh [days]` (default `30`) consulta `registry.db` y falla con codigo != 0 si alguna funcion C++ creada en los ultimos N dias no tiene `tested: true` en su frontmatter. Esta hookeado al final de `cpp/scripts/run_tests.sh`, por lo que el flujo CI (`./scripts/run_tests.sh`) falla si se anade una funcion C++ nueva sin test asociado. Para satisfacer el gate: 1. Crear `cpp/tests/test_.cpp` (puede ser placeholder Catch2 si la logica visual se cubre via `primitives_gallery`). 2. Anadirlo a `cpp/tests/CMakeLists.txt` con `add_fn_test(test_ ...)`. 3. Marcar `tested: true` + `test_file_path: cpp/tests/test_.cpp` en el frontmatter del `.md` de la funcion. 4. Correr `fn index` para refrescar `registry.db`.