Snapshot de WIP acumulado de sesiones previas antes de merge wave 1 del flow 0008 (kanban_cpp + agent_runner_api + DoD schema). Incluye: - dev/flows/0008-kanban-cpp-and-agent-workflows.md - dev/issues/0112-0119*.md (7 sub-issues) - WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23 KiB
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 <name> [--project <p>] [--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.mdvalido (framework, dir_path, repo_url) parafn index. - Registra
add_subdirectoryencpp/CMakeLists.txt(raiz o bloque_DIRpara projects). - Crea repo Gitea
dataforge/<name>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/<nombre>/ |
| App de un proyecto | projects/<proyecto>/apps/<nombre>/ |
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
<app_dir>/
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
[<modulo>.{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.cppSIEMPRE — punto de entrada conint main()+fn::run_app(...)+ funcionrender().- Si la app supera ~400 lineas en
main.cpp, partir endata.{h,cpp}(carga/persistencia) +views.{h,cpp}(UI por panel). - Modulos especificos del dominio en archivos propios (
compiler.cppenshaders_lab,data_http.cppenregistry_dashboard). - NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (
cpp/functions/...).
3. CMakeLists.txt
Patron canonico:
add_imgui_app(<target>
main.cpp
[extra_modules.cpp]
# Funciones del registry usadas (paths absolutos):
${CMAKE_SOURCE_DIR}/functions/<dominio>/<funcion>.cpp
...
)
target_include_directories(<target> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(<target> PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...)
if(WIN32)
set_target_properties(<target> PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
Reglas:
- Usar SIEMPRE la macro
add_imgui_app(target ...)— gestiona enlace confn_frameworky copia de TTFs. - Listar explicitamente cada
.cppdel 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 enfn_frameworky dan multiple-definition si se duplican. - En
WIN32, marcarWIN32_EXECUTABLE TRUEpara apps GUI (sin consola).
4. app.md (frontmatter)
Plantilla minima para apps C++:
---
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
version: 0.1.0 # semver per-app, bumped via /version
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++
- <nombre>_cpp_<dominio>
- ...
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/<name>" o "projects/<proyecto>/apps/<name>"
repo_url: "https://gitea-.../dataforge/<name>"
---
Reglas:
uses_functionsse rellena a mano con los IDs de las funciones del registry usadas enCMakeLists.txt. Auditar con:sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';"+ revisar diffs.framework: "imgui"siempre que usefn::run_app. Otros valores solo si la app NO usa el shell (raro).tags: incluirservicesi es daemon de larga duracion (verfunction_tags.md).repo_urlapunta al sub-repo en Gitea (ver §6).version: semver per-app. Baseline0.1.0para apps nuevas. Bump obligatorio via/version apps/<name> {major|minor|patch} "<reason>"cuando/fix-issuetoque codigo de la app. Trazabilidad humana en seccion## Capability growth logal final delapp.md(una linea por bump). Ver.claude/commands/version.md.
5. Registro en cpp/CMakeLists.txt
Cada app nueva se registra al final de cpp/CMakeLists.txt:
# --- <app_name> ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/<name>/CMakeLists.txt)
add_subdirectory(apps/<name>)
endif()
Para apps en proyectos (fuera del arbol cpp/):
# --- <app_name> (lives in projects/<proj>/apps/) ---
set(_<NAME>_DIR ${CMAKE_SOURCE_DIR}/../projects/<proj>/apps/<name>)
if(EXISTS ${_<NAME>_DIR}/CMakeLists.txt)
add_subdirectory(${_<NAME>_DIR} ${CMAKE_BINARY_DIR}/apps/<name>)
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/<name> con branch master. Esto significa:
- El directorio
<app_dir>/esta en el.gitignoredefn_registry(exceptoapp.md). - El propio directorio tiene
.git/apuntando al sub-repo. - TBD obligatorio mientras se desarrolla la app: ver
apps_tbd.md. Trabajar enissue/<NNNN>-<slug>oquick/<slug>, mergear amastercon--no-ff. - Sync entre PCs y push/pull se gestionan con
/full-git-pushy/full-git-pull.
7. Convencion local_files/ — separacion de distribuible vs estado local
OBLIGATORIO: TODA app coloca sus archivos escribibles bajo
<exe_dir>/local_files/. Los archivos distribuibles (.exe, .dll,
.ttf, enrichers/, runtime/) viven directos en <exe_dir>/.
<exe_dir>/
├── <app>.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_*
└── <lo que la app escriba> ← usar fn::local_path("nombre")
fn::run_app lo gestiona automaticamente para imgui.ini y
app_settings.ini y migra desde <exe_dir>/ 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:
// 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()—<exe_dir>/local_files/, creado on-demand.fn::local_path(name)—<local_dir>/<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:
-
GLFW pos/size callbacks —
vp->Pos/Sizese sincronizan al instante conglfwSetWindowPos/Size(no espera al siguiente NewFrame). -
Per-frame viewport sync al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente.
-
Win32 WndProc subclass per HWND (
#ifdef _WIN32) — observaWM_ENTERSIZEMOVE/WM_EXITSIZEMOVEque 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 depio.Viewports). Mientras el bracket esta abierto en CUALQUIER HWND propio, el main loop SKIPEArender_fn+glfwSwapBuffersglobalmente, replicando el contrato del title-bar drag native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer existente). El flagg_in_sizemovees 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<HWND, WNDPROC>. Chain a la proc original viaCallWindowProcW.install_sizemove_subclass_hwnd(HWND)idempotente (skip si ya en mapa).- Per-frame:
prune_dead_subclassed()conIsWindow+ install en cadapio.Viewports[i]->PlatformHandlenuevo. 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 emitePostMessage(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:
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): drivesglfwSetWindowPoscada frame, assertavp->Possigue OS dentro de 1px.p2.altsnap(Windows): worker thread fakeaWM_ENTERSIZEMOVE+ burst deSetWindowPos(SWP_ASYNCWINDOWPOS)+WM_EXITSIZEMOVEsobre el HWND principal, asserta querender()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 — capturaIsWindow(secondary_hwnd)antes/durante/despues deglfwIconifyWindow + glfwRestoreWindow. Asserta los 3 estados vivos yrenders_iconified > 0.p5.alt_rmb(Windows):set_force_alt_for_test(true)+SendMessage(WM_RBUTTONDOWN)sincrono mismo-hilo. Assertaalt_rmb_resize_countincrementa.p6.alt_lmb(Windows): mismo patron paraWM_LBUTTONDOWN. Assertaalt_lmb_move_countincrementa.
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 | <app_dir>/<app>.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_<dominio>.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;falsesi la app necesita estar siempre embebida.init_gl_loader:truesi llamagl*directo (renderers GPU custom comograph_renderer);falsesi solo usa ImGui/ImPlot.aboutinfo: nombre, version (semver), descripcion 1 frase.- Persistencia:
<app>.dbSQLite junto al exe; nunca tocarregistry.dbnioperations.dbsalvo lectura. - Modo CLI: si la app acepta args, documentarlos en el
app.mdcon ejemplos.
10. Layouts persistentes (default)
fn::run_app provee menu Layouts (Save current as.../Apply/Delete/Reset) sin
codigo. Crea <exe_dir>/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 comoshaders_lab): abre su propioLayoutStorage, llamalayout_storage_make_callbacks, overrideon_reset, y pasacfg.layouts_cb = &cb. Cuando se pasalayouts_cb, el auto-storage se desactiva y la app es responsable delayout_storage_apply_pendingal inicio de surender. - App headless / capture mode:
cfg.auto_layouts = false. - Cambiar nombre del archivo:
cfg.auto_layouts_db = "<algo>.db"(relativo alocal_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:
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 vive en el frontmatter de cada app.md C++:
description: "Frase corta de 1 linea — que hace la app y por que existe."
icon:
phosphor: "chart-bar"
accent: "#0ea5e9"
Trio obligatorio: description + icon.phosphor + icon.accent
REGLA DURA: TODA app C++/imgui declara los 3 campos JUNTOS en su app.md:
description:(string corta, 1 linea) — texto que elapp_hub_launchermuestra en la tarjeta y que el dashboard usa para tooltips.icon.phosphor:(nombre del glyph Phosphor sin sufijo-fill) — glyph del icono.icon.accent:(hex#rrggbb) — color del fondo redondeado del icono Y color del boton/border de la tarjeta enapp_hub_launcher.
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin icon: cae al default (app-window slate); sin accent el boton del hub aparece blanco. Documentar uno sin los otros es bug, no estilo.
Refrescar el App Hub tras editar el trio
app_hub_launcher cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar description/icon.* en un app.md requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
./fn run refresh_app_hub # icons + manifest + restart hub
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
ID: refresh_app_hub_bash_pipelines. Compone export_hub_icons_py_infra + export_hub_manifest_py_infra + is_cpp_app_running_windows_bash_infra + launch_cpp_app_windows_bash_infra.
Regeneracion batch via pipeline del registry — escanea app.mds y compone
generate_app_icon por app. Anadir app nueva: declarar icon: en su
app.md y lanzar:
./fn run regenerate_app_icons # todas
./fn run regenerate_app_icons chart_demo # solo una
Convenciones:
- Glyph weight:
fill(mas legible a 16px queregularobold). - Color: 1 accent_hex distinto por app — Tailwind palette 500-700
funciona bien (
#0ea5e9sky-500,#16a34agreen-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
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
./fn run regenerate_app_icons chart_demo
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
# 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.
Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
Embeber .ico en el .exe (windres) basta para File Explorer / shortcuts —
pero GLFW crea su WNDCLASS sin icono, asi que la barra de tareas, el
header de la ventana y Alt+Tab muestran el icono GLFW por defecto a
menos que adjuntemos el recurso al HWND en runtime.
fn::run_app lo hace automaticamente, sin opt-in. Tras glfwCreateWindow:
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
Resource ID 101 lo emite add_imgui_app en el .rc generado
(101 ICON "<app_dir>/appicon.ico"). Si la app no tiene appicon.ico, el
.rc no se genera, LoadImageW devuelve NULL y el HWND queda con el icono
GLFW por defecto (sin error).
Cobertura multi-viewport: el per-frame scan de pio.Viewports (mismo que
instala el sizemove subclass) tambien llama attach_app_icon_to_hwnd sobre
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
sin codigo extra en la app.
Cache shell: el pipeline redeploy_cpp_app_windows llama
refresh_windows_icon_cache_bash_infra tras copiar el .exe — invoca
ie4uinit.exe -show para que Explorer recargue iconcache.db sin esperar
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
icono viejo: borrar %LOCALAPPDATA%\IconCache.db + reiniciar Explorer.