## Estandarizacion de apps C++ del registry **Fuentes autoritativas:** - `cpp/PATTERNS.md` — checklist y esqueleto del app shell (`fn::run_app`, AppConfig, panels, layouts, Settings, About). - `cpp/DESIGN_SYSTEM.md` — identidad visual (`fn_tokens`, ThemeMode, equivalencias `@fn_library` ↔ C++). Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli. ### 1. Ubicacion | Caso | Donde vive | |---|---| | App independiente | `cpp/apps//` | | App de un proyecto | `projects//apps//` | NUNCA en `cpp/apps//` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. ### 2. Estructura minima ``` / CMakeLists.txt # usa add_imgui_app(target ...) app.md # frontmatter de registro (ver §4) main.cpp # entry: parseo de args + fn::run_app + render() [data.{h,cpp}] # opcional: capa de datos (DB / HTTP / archivos) [views.{h,cpp}] # opcional: composicion de paneles [.{h,cpp}] # opcional: dominio especifico [vendor/] # opcional: deps no comunes (se prefieren las globales en cpp/vendor/) [.git/] # cada app es su propio repo Gitea (ver §6) ``` **Reglas de split:** - `main.cpp` SIEMPRE — punto de entrada con `int main()` + `fn::run_app(...)` + funcion `render()`. - Si la app supera ~400 lineas en `main.cpp`, partir en `data.{h,cpp}` (carga/persistencia) + `views.{h,cpp}` (UI por panel). - Modulos especificos del dominio en archivos propios (`compiler.cpp` en `shaders_lab`, `data_http.cpp` en `registry_dashboard`). - NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (`cpp/functions/...`). ### 3. CMakeLists.txt Patron canonico: ```cmake add_imgui_app( main.cpp [extra_modules.cpp] # Funciones del registry usadas (paths absolutos): ${CMAKE_SOURCE_DIR}/functions//.cpp ... ) target_include_directories( PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries( PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...) if(WIN32) set_target_properties( PROPERTIES WIN32_EXECUTABLE TRUE) endif() ``` Reglas: - Usar SIEMPRE la macro `add_imgui_app(target ...)` — gestiona enlace con `fn_framework` y copia de TTFs. - Listar explicitamente cada `.cpp` del registry usado (no glob). Hace visible el grafo de dependencias. - NO listar `tokens.cpp`, `icon_font.cpp`, `app_settings.cpp`, `app_about.cpp`, `fps_overlay.cpp`, `panel_menu.cpp`, `app_menubar.cpp`, `layouts_menu.cpp`, `gl_loader.cpp`, `layout_storage.cpp` — viven en `fn_framework` y dan multiple-definition si se duplican. - En `WIN32`, marcar `WIN32_EXECUTABLE TRUE` para apps GUI (sin consola). ### 4. app.md (frontmatter) Plantilla minima para apps C++: ```yaml --- name: lang: cpp domain: description: "Frase corta — lo que hace y por que existe." tags: [imgui, ...] # si es service, anadir 'service' uses_functions: # IDs del registry — el indexer NO deduce C++ - _cpp_ - ... uses_types: [] framework: "imgui" entry_point: "main.cpp" dir_path: "cpp/apps/" o "projects//apps/" repo_url: "https://gitea-.../dataforge/" --- ``` Reglas: - `uses_functions` se rellena a mano con los IDs de las funciones del registry usadas en `CMakeLists.txt`. Auditar con: `sqlite3 registry.db "SELECT id FROM apps WHERE id='';"` + revisar diffs. - `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro). - `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`). - `repo_url` apunta al sub-repo en Gitea (ver §6). ### 5. Registro en `cpp/CMakeLists.txt` Cada app nueva se registra al final de `cpp/CMakeLists.txt`: ```cmake # --- --- if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps//CMakeLists.txt) add_subdirectory(apps/) endif() ``` Para apps en proyectos (fuera del arbol `cpp/`): ```cmake # --- (lives in projects//apps/) --- set(__DIR ${CMAKE_SOURCE_DIR}/../projects//apps/) if(EXISTS ${__DIR}/CMakeLists.txt) add_subdirectory(${__DIR} ${CMAKE_BINARY_DIR}/apps/) endif() ``` El `if(EXISTS ...)` hace el registro tolerante a apps no clonadas (cada app es sub-repo separado). ### 6. Sub-repo Gitea (TBD obligatorio) Cada app C++ es su propio repo en `dataforge/` con branch `master`. Esto significa: - El directorio `/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`). - El propio directorio tiene `.git/` apuntando al sub-repo. - TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/-` o `quick/`, mergear a `master` con `--no-ff`. - Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`. ### 7. Convencion `local_files/` — separacion de distribuible vs estado local **OBLIGATORIO**: TODA app coloca sus archivos escribibles bajo `/local_files/`. Los archivos distribuibles (`.exe`, `.dll`, `.ttf`, `enrichers/`, `runtime/`) viven directos en `/`. ``` / ├── .exe ├── duckdb.dll, *.ttf, runtime/, enrichers/ ← read-only, ships con el zip └── local_files/ ← writable, per-PC ├── imgui.ini ← gestionado por fn::run_app ├── app_settings.ini ← gestionado por fn_ui::settings_* └── ← usar fn::local_path("nombre") ``` `fn::run_app` lo gestiona automaticamente para `imgui.ini` y `app_settings.ini` y migra desde `/` o `cwd` si vienen de una version previa. Apps que escriban archivos extra (DBs, caches, proyectos del usuario) **DEBEN** usar `fn::local_path("nombre")` al construir sus paths. Ejemplo: ```cpp // MAL sqlite3_open("graph_explorer.db", &db); fopen("graph_explorer.ini", "r"); // BIEN sqlite3_open(fn::local_path("graph_explorer.db"), &db); fopen(fn::local_path("graph_explorer.ini"), "r"); ``` API en `cpp/framework/app_base.h`: - `fn::exe_dir()` — directorio del ejecutable. - `fn::local_dir()` — `/local_files/`, creado on-demand. - `fn::local_path(name)` — `/`. - `fn::migrate_to_local_files(names, n)` — mueve archivos viejos. Beneficios: - Carpeta del .exe limpia para distribuir (zip portable). - Reset trivial (basta borrar `local_files/`). - Separacion clara para backup/sync (solo `local_files/` es propio del PC). ### 7.1 Anti-jitter automatico (AltSnap, tiling WMs) `fn::run_app` aplica tres capas de proteccion contra jitter al mover la ventana con herramientas externas (AltSnap en Windows, snap-assist, tiling WMs). Activado por defecto, sin opt-in: 1. **GLFW pos/size callbacks** — `vp->Pos/Size` se sincronizan al instante con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame). 2. **Per-frame viewport sync** al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente. 3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`, replicando el contrato del title-bar drag native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer existente). Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases: - `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta `vp->Pos` sigue OS dentro de 1px. - `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` + burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta que `render()` no se llama durante el bracket. Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`. NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app necesita renderizar incluso durante external move (caso raro: telemetria en vivo, video stream), tendria que evitar el bypass — actualmente no hay flag para desactivarlo (anadir `cfg.pause_on_external_sizemove = true` por default si surge necesidad). ### 8. Convenciones de runtime Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app: | Anti-patron | Sustituir por | |---|---| | `glfwInit()` en `main` | `fn::run_app(cfg, render)` | | `ImGui::StyleColorsDark()` | `cfg.theme = ThemeMode::FnDark` (default) | | `ImVec4(0.5,0.5,0.5,1)` | `fn_tokens::colors::*` | | `ImGui::Begin(u8"\xEF...")` | `ImGui::Begin(TI_HOME " ...")` | | Menubar inline cada frame | `cfg.panels` + `cfg.layouts_cb` | | About hardcoded en un panel | `cfg.about = {...}` | | `gl*` directo sin loader | `cfg.init_gl_loader = true` | | Tabla SQLite en la raiz del repo | `/.db` (operations.db es solo para entities/relations/executions) | | `fopen("foo.ini", ...)` con path relativo | `fopen(fn::local_path("foo.ini"), ...)` (ver §7) | ### 8. Tests visuales (recomendado, no obligatorio) Si la app tiene componentes que se quieren proteger contra regresiones visuales, anadir un demo en `cpp/apps/primitives_gallery/demos_.cpp` que use los mismos componentes/funciones del registry. El sistema de capture-and-compare de `primitives_gallery --capture` funciona como golden-image gate (ver final de `cpp/PATTERNS.md`). ### 9. Decisiones que cada app debe tomar y documentar en su `app.md` - `viewports`: `true` (default) si las ventanas pueden arrastrarse fuera del main; `false` si la app necesita estar siempre embebida. - `init_gl_loader`: `true` si llama `gl*` directo (renderers GPU custom como `graph_renderer`); `false` si solo usa ImGui/ImPlot. - `about` info: nombre, version (semver), descripcion 1 frase. - Persistencia: `.db` SQLite junto al exe; nunca tocar `registry.db` ni `operations.db` salvo lectura. - Modo CLI: si la app acepta args, documentarlos en el `app.md` con ejemplos. ### 10. Layouts persistentes (default) `fn::run_app` provee menu Layouts (Save current as.../Apply/Delete/Reset) sin codigo. Crea `/local_files/layouts.db` (tabla `imgui_layouts` + `layout_meta`) y persiste el `imgui.ini` serializado por nombre. **Restore-on-open / save-on-close (1.1.0+):** al cerrar la app, el slot del layout activo se reescribe con el `imgui.ini` actual (los retoques de docking sobreviven). Al abrir, si habia un layout activo persistido en `layout_meta.last_active`, se carga en el primer frame. Si la app no usa named layouts (nunca clico Save/Apply), el comportamiento sigue siendo el de antes: `imgui.ini` es la unica fuente. - App nueva: nada que tocar — Layouts viene activo. - App quiere personalizar `on_reset` (ej. re-mostrar paneles especificos como `shaders_lab`): abre su propio `LayoutStorage`, llama `layout_storage_make_callbacks`, override `on_reset`, y pasa `cfg.layouts_cb = &cb`. Cuando se pasa `layouts_cb`, el auto-storage se desactiva y la app es responsable de `layout_storage_apply_pending` al inicio de su `render`. - App headless / capture mode: `cfg.auto_layouts = false`. - Cambiar nombre del archivo: `cfg.auto_layouts_db = ".db"` (relativo a `local_files/`).