Files
egutierrez 516db8efc0 feat(infra): auto-commit con 10 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 16:56:53 +02:00

9.9 KiB

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 <exe_dir>/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.
  • Auto-dockspace (default true). El framework llama ImGui::DockSpaceOverViewport(0, GetMainViewport(), PassthruCentralNode) antes de render_fn() cada frame. NO llamar DockSpaceOverViewport manual en render() — duplica nodes y causa flicker. Apps que usan layout custom con ImGui::DockSpace propio o fullscreen_window deben poner cfg.auto_dockspace = false.
  • No fn_ui::app_menubar(...) manual. El framework ya lo dibuja en cada frame leyendo cfg.panels/cfg.layouts_cb/cfg.view_extras. Llamarlo manualmente provoca barra duplicada o pisada.
  • 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/<nombre>). Sin warnings nuevos.

Crear app nueva — usar el scaffolder

# App suelta en cpp/apps/<name>/
fn run init_cpp_app my_tool --desc "Herramienta para X"

# App dentro de un proyecto
fn run init_cpp_app finance_panel --project budget --desc "Panel de finanzas"

init_cpp_app_bash_pipelines genera la estructura canonica (main.cpp + CMakeLists.txt + app.md) cumpliendo este documento, registra la app en cpp/CMakeLists.txt, crea repo Gitea dataforge/<name> y ejecuta fn index. Despues solo se completa uses_functions cuando se importan funciones del registry.

Esqueleto minimo

#include "framework/app_base.h"
#include "core/icons_tabler.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() {
    // Sin DockSpaceOverViewport ni app_menubar manual — los da el framework.
    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.log            = {"my_app.log", 1};
    cfg.panels         = k_panels;
    cfg.panel_count    = sizeof(k_panels) / sizeof(k_panels[0]);
    cfg.init_gl_loader = false; // true si usas OpenGL directo
    // cfg.auto_dockspace = false; // solo si gestionas DockSpace propio (ej. shaders_lab)
    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 <exe_dir>/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
fn_ui::app_menubar(nullptr,0,nullptr) en render El framework ya lo dibuja
ImGui::DockSpaceOverViewport(...) en render auto_dockspace=true por defecto
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 <output_dir> que renderiza cada demo en una ventana GLFW invisible y guarda un PNG por demo. Se usa para tests visuales tipo golden-image:

# 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/<demo>.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/<demo>.png vs el golden en cpp/tests/golden/<demo>.png.

Tests de UI headless (Dear ImGui Test Engine)

fn::run_app_test (el harness del Test Engine usado por /e2e-cpp) crea la ventana GLFW oculta por defecto (GLFW_VISIBLE=FALSE). El contexto OpenGL real se crea igual, así que el render que el Test Engine ejercita sigue siendo fiel, pero la ventana nunca se mapea en pantalla: cero parpadeo y no roba foco mientras corre la suite. Es el comportamiento preferente para tests de frontend en C++.

Control del modo (en orden de prioridad):

Mecanismo Efecto
FN_HEADLESS=0 (env) Fuerza ventana visible — para depurar un test a ojo.
FN_HEADLESS=1 (env) Fuerza oculta (es el default del path de test).
cfg.headless = true Oculta también fn::run_app (apps reales, p.ej. smoke/capture).
sin nada run_app_test → oculta; run_app → visible.

Cómo correr la suite sin parpadeo:

# Host con GL nativo (GPU real): binario directo, ventana oculta, sin parpadeo.
./build/linux_tests/apps/<app>/<app>_tests

# CI / WSL sin display: display virtual en RAM (también headless).
xvfb-run -a -s "-screen 0 1280x800x24" \
    env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
    ./build/linux_tests/apps/<app>/<app>_tests

# Ver un test a ojo (desactiva headless):
FN_HEADLESS=0 ./build/linux_tests/apps/<app>/<app>_tests

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_<name>.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_<name> ...).
  3. Marcar tested: true + test_file_path: cpp/tests/test_<name>.cpp en el frontmatter del .md de la funcion.
  4. Correr fn index para refrescar registry.db.