chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md - .claude/commands/fn_claude.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - .claude/rules/ids_naming.md - CHANGELOG.md - apps/dag_engine/README.md - apps/dag_engine/api.go - apps/dag_engine/dags_migrated/example.yaml - apps/dag_engine/dags_migrated/example_lineage_tracking.yaml - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+162
-12
@@ -20,14 +20,14 @@ Razones:
|
||||
|
||||
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
|
||||
|
||||
### 1. Ubicacion
|
||||
### 1. Ubicacion (issue 0096 estandarizada)
|
||||
|
||||
| Caso | Donde vive |
|
||||
|---|---|
|
||||
| App independiente | `cpp/apps/<nombre>/` |
|
||||
| App independiente | `apps/<nombre>/` |
|
||||
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
|
||||
|
||||
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
NUNCA en `cpp/apps/<nombre>/` (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
|
||||
|
||||
@@ -189,20 +189,105 @@ WMs). Activado por defecto, sin opt-in:
|
||||
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).
|
||||
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.
|
||||
|
||||
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
|
||||
Estado del subclass:
|
||||
- `g_subclassed` = `unordered_map<HWND, WNDPROC>`. 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`, asserta
|
||||
que `render()` no se llama durante el bracket.
|
||||
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 `e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
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
|
||||
@@ -261,3 +346,68 @@ de antes: `imgui.ini` es la unica fuente.
|
||||
- App headless / capture mode: `cfg.auto_layouts = false`.
|
||||
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.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
|
||||
`<app_dir>/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 `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
|
||||
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
|
||||
`IDI_ICON1 ICON "<path>"` 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 <app>.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 <keyword>` 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.
|
||||
|
||||
Reference in New Issue
Block a user