## 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. ### Scaffolder canonico — OBLIGATORIO **REGLA DURA:** crear apps C++ nuevas SIEMPRE con `fn run init_cpp_app [--project

] [--desc "..."]`. NUNCA escribir `main.cpp` + `CMakeLists.txt` + `app.md` desde cero a mano en `cpp/apps/` ni `projects/*/apps/`. Tampoco copiar otra app y renombrar — la deriva entre patrones es lo que estamos eliminando. Si el scaffolder no cubre un caso (ej. necesitas plantilla diferente, layout custom desde el primer dia), **modificas el scaffolder**, no escribes la app a mano. La plantilla canonica es codigo, no decoracion. Razones: - Garantiza `cfg.about` + `cfg.log` + `cfg.panels` + framework defaults aplicados. - Genera frontmatter `app.md` valido (framework, dir_path, repo_url) para `fn index`. - Registra `add_subdirectory` en `cpp/CMakeLists.txt` (raiz o bloque `_DIR` para projects). - Crea repo Gitea `dataforge/` con master + commit inicial. Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`. ### 1. Ubicacion (issue 0096 estandarizada) | Caso | Donde vive | |---|---| | App independiente | `apps//` | | App de un proyecto | `projects//apps//` | NUNCA en `cpp/apps//` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. 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 per HWND** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. El subclass se instala en la ventana principal Y en cada HWND secundario que el backend de ImGui crea cuando un panel se arrastra fuera del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` + `glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer existente). El flag `g_in_sizemove` es global a proposito: una sola sesion de sizemove externo pausa todo el render para que ninguna ventana compita con el OS. Estado del subclass: - `g_subclassed` = `unordered_map`. Chain a la proc original via `CallWindowProcW`. - `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa). - Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada `pio.Viewports[i]->PlatformHandle` nuevo. - `uninstall_sizemove_subclass_all()` restaura cada HWND al exit. #### Iconified main no pierde paneles flotantes (2026-05-16) El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO el frame loop. Con multi-viewport activo eso significa que `ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de refrescar los viewports secundarios — los floating panels aparecen congelados o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta viewports secundarios primero; si hay alguno, fall-through al frame normal (la swap del main HWND minimizado es harmless, los contexts GL secundarios siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`. #### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16) WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`): - `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` + `PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo. - `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/ BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite `PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo. Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a cada viewport flotante porque el subclass per-frame ya cubre todos los HWND. El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa render → cero jitter automatico, mismo contrato que el title-bar drag. **Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut UI. Aceptable porque Alt-modifier en clicks UI es muy raro. #### Title-bar-only move para ImGui windows (2026-05-16) `fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico para viewports secundarios: un viewport flotante = OS window borderless con UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport entero, el OS window sigue al cursor sin modifier. Con el flag, floating panels obedecen el contrato "solo header arrastra" (igual que main que tiene title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido antes por el subclass). #### Test observability — `fn::internal::*` (2026-05-16) Counters monotonicos para validar el subclass desde tests headless, zero-cost en prod: ```cpp namespace fn::internal { int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests } ``` En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el modal de Windows. Path real en prod sigue posteandolos. Tests: `apps/altsnap_jitter_test/` corre seis 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` sobre el HWND principal, asserta que `render()` no se llama durante el bracket. - `p3.secondary` (Windows): fuerza viewport secundario (`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket sobre el. Valida que el subclass per-viewport tambien pausa el render. - `p4.minimize` (Windows): state machine 4 steps — captura `IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow + glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`. - `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` + `SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta `alt_rmb_resize_count` incrementa. - `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta `alt_lmb_move_count` incrementa. Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh && 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/`). ### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16 Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en `/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro `add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si `WIN32` + existe `/appicon.ico`, genera un `_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con `IDI_ICON1 ICON ""` y lo anade a `add_executable`. El compilador RC (`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`) lo enlaza al `.exe` como recurso `.rsrc`. Verificar: `x86_64-w64-mingw32-objdump -h .exe | grep rsrc` debe mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara `LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`). #### Crear `.ico` para una app nueva Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de `https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`, `bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor legibilidad a 16/24px. Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una linea por app: ```python from infra import generate_app_icon generate_app_icon( phosphor_icon_name="chart-bar", accent_hex="#0ea5e9", out_ico_path="apps/chart_demo/appicon.ico", ) ``` Mapping inicial (2026-05-16) en `dev/gen_app_icons.py` — script reproducible que regenera los 11 `.ico` de un golpe leyendo la tabla `APPS`. Anadir app nueva: una fila `(app_id, dir, phosphor_icon, accent_hex)` en `APPS` y `/tmp/iconenv/bin/python dev/gen_app_icons.py` (o el venv del registry, ya trae `cairosvg` + `Pillow`). Convenciones: - **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`). - **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700 funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.). - **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado. - **Glyph color**: siempre blanco sobre el fondo accent. Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/` con `ls | grep ` antes de inventar — 1512 disponibles. #### Re-deploy tras cambiar icono ```bash # 1. Regenerar .ico ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" # (o editar dev/gen_app_icons.py + relanzar) # 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc) ./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build ``` Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.