42c14fae59
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
8.6 KiB
Markdown
187 lines
8.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```cpp
|
|
#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:
|
|
|
|
```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/<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`.
|
|
|
|
### 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`.
|