# fn_registry C++ — Design System > **Fuente de verdad de la identidad visual para todas las apps C++ del registry.** > Espejo directo de `frontend/DESIGN_SYSTEM.md` en mundo ImGui / OpenGL. --- ## 1. Principio Todas las apps C++ del registry comparten **la misma identidad visual que las apps web**: Mantine v9 dark + indigo primary + Geist typography + `radius.md = 8` por defecto. No existen apps C++ con tema propio — si una app quiere diferenciarse, lo hace eligiendo colores semanticos (success, warning, primary...), nunca redefiniendo la paleta base. El equivalente C++ de `FnMantineProvider` + `createTheme(...)` es **una sola llamada**: ```cpp fn::run_app(config, render); // AppConfig::theme = ThemeMode::FnDark por defecto ``` Dentro, `app_base` invoca `fn_tokens::apply_dark_theme()` tras crear el contexto ImGui. Equivale exactamente a: ```tsx ``` --- ## 2. Stack | Capa | Libreria | Notas | |------|----------|-------| | UI base | **Dear ImGui** (vendored) | immediate mode, docking branch | | Charts | **ImPlot** (vendored) | line/bar/pie/histogram/heatmap/sparkline | | Node editor | `imgui-node-editor` (vendored) | solo para `shader_canvas` / DAGs | | Gráficos gfx | OpenGL 3.3 core via GLFW | `gl_shader`, `gl_framebuffer`, `fullscreen_quad` | | Profiling | Tracy (opt-in con `TRACY_ENABLE=ON`) | `tracy_zone_cpp_core` | | Tokens | `cpp/functions/core/tokens.{h,cpp}` | equivalente a `@fn_library` | | Componentes | `cpp/functions/core/*`, `viz/*`, `gfx/*` | wrappers que leen `fn_tokens` | | Framework | `cpp/framework/app_base.{h,cpp}` | bootstrap unico de GLFW + ImGui + tema | | Build | CMake 3.16+ | un `add_subdirectory` por app | ### Deny-list (no usar en apps C++) - No `ImGui::StyleColorsDark/Light/Classic()` en el main de una app — el tema lo fija `app_base` - No `ImVec4(...)` hardcoded en widgets — usar `fn_tokens::colors::*` - No pixeles magicos para spacing/radius — usar `fn_tokens::spacing::*` / `radius::*` - No ICU / gettext — textos directos, UTF-8 en el fuente - No libs de UI ajenas (Qt, Nuklear, Gtk, etc.) - No CSS, QSS ni recursos externos de estilo --- ## 3. `AppConfig` y modos de tema ```cpp #include "app_base.h" int main() { fn::AppConfig cfg; cfg.title = "my_app"; cfg.width = 1400; cfg.height = 900; cfg.viewports = true; // multi-viewport OK, el tema se mantiene // cfg.theme = fn::ThemeMode::FnDark; // ya es el default return fn::run_app(cfg, render); } ``` | `ThemeMode` | Uso | |---|---| | `FnDark` (**default**) | Identidad del registry — Mantine v9 dark + indigo | | `ImGuiDark` | Dark de stock (gris, rounding 0). Solo si estas depurando estilos | | `ImGuiLight` | Light de stock. Para capturas o documentacion | | `None` | No toca `ImGuiStyle`. La app se encarga (evitar salvo caso excepcional) | **Regla:** nunca cambies `ThemeMode` salvo que tengas justificacion; si tu app necesita variaciones, expon-las como props semanticos (`kind: "danger"`, `variant: "subtle"`) en tus widgets, no como tema alternativo. --- ## 4. Tokens — `fn_tokens` Todos en `cpp/functions/core/tokens.h`. Namespaces: ### 4.1 Colors — Mantine v9 dark + indigo | Token | Mantine | Hex | Uso | |---|---|---|---| | `colors::primary` | indigo.6 | `#4C6EF5` | Botones, tabs activos, selecciones | | `colors::primary_hover` | indigo.5 | `#5C7CFA` | Hover de primary | | `colors::primary_light` | indigo.4 | `#748FFC` | Nav highlight, checkmarks, separator activo | | `colors::primary_active` | indigo.7 | `#4263EB` | Pulsado | | `colors::success` | green.6 | `#40C057` | Badges / toasts de exito | | `colors::warning` | yellow.6 | `#FAB005` | Avisos | | `colors::error` | red.6 | `#FA5252` | Errores, destructive | | `colors::info` | blue.6 | `#228BE6` | Info, enlaces | | `colors::bg` | dark.7 | `#1A1B1E` | Window bg (= Mantine body) | | `colors::surface` | dark.6 | `#25262B` | Paper, Card, FrameBg | | `colors::surface_hover` | dark.5 | `#2C2E33` | Hover de surface | | `colors::surface_active` | dark.4 | `#373A40` | Pulsado de surface | | `colors::border` | dark.4 | `#373A40` | Borders estandar | | `colors::border_strong` | dark.3 | `#5C5F66` | Scrollbar grab active | | `colors::text` | dark.0 | `#C1C2C5` | Texto primario | | `colors::text_muted` | dark.2 | `#909296` | Subtitulos, axis labels | | `colors::text_dim` | dark.3 | `#5C5F66` | Disabled | ### 4.2 Radius — `defaultRadius: 'md'` | Token | Valor | Equivalente Mantine | |---|---|---| | `radius::none` | 0 | — | | `radius::xs` | 2 | `--mantine-radius-xs` | | `radius::sm` | 4 | `--mantine-radius-sm` | | `radius::md` | **8** | `--mantine-radius-md` (**default**) | | `radius::lg` | 12 | `--mantine-radius-lg` (adaptado, Mantine usa 16) | | `radius::xl` | 16 | `--mantine-radius-xl` (adaptado, Mantine usa 32) | ImGui aplica `md` en `WindowRounding`, `ChildRounding`, `PopupRounding`, `FrameRounding`, `ScrollbarRounding`. `sm` en `GrabRounding`, `TabRounding`. ### 4.3 Spacing — densificado para ImGui Mantine usa 10/12/16/20/32 px para CSS; ImGui vive en densidad mayor (TUI-ish), asi que el mapeo se densifica: | Token | ImGui | Mantine CSS | |---|---|---| | `spacing::xs` | 4 | 10 | | `spacing::sm` | 8 | 12 | | `spacing::md` | 12 | 16 | | `spacing::lg` | 16 | 20 | | `spacing::xl` | 24 | 32 | Se usan en `WindowPadding`, `FramePadding`, `ItemSpacing`, `CellPadding`, `IndentSpacing`. ### 4.4 Font size `font_size::xs|sm|md|lg|xl|xxl` = `10|12|14|18|24|32`. El default (14) coincide con `--mantine-font-size-sm` (tiempos mas compactos que el body web de 16). --- ## 5. Qué aplica `apply_dark_theme()` Se invoca una vez desde `app_base` (o manualmente si usas `ThemeMode::None`). Configura: - **Todos los `ImGuiCol_*`**: WindowBg, ChildBg, PopupBg, FrameBg (+Hovered/Active), Button (+Hovered/Active), Header (+Hovered/Active), Tab (+Hovered/Active/Unfocused), Separator (+Hovered/Active), Border, Text (+Disabled/Selected), CheckMark, SliderGrab (+Active), ResizeGrip (+Hovered/Active), Scrollbar (+Grab +GrabHovered +GrabActive), Table (Header +BorderLight +BorderStrong +RowBg +RowBgAlt), Docking (Preview +EmptyBg), PlotLines/PlotHistogram, DragDropTarget, Nav (Highlight +WindowingHighlight +WindowingDimBg), ModalWindowDimBg, TextSelectedBg, MenuBarBg, TitleBg (+Active +Collapsed) - **Rounding**: Window, Child, Popup, Frame, Grab, Scrollbar, Tab - **Padding/spacing**: WindowPadding, FramePadding, CellPadding, ItemSpacing, ItemInnerSpacing, IndentSpacing, ScrollbarSize, GrabMinSize - **Borders**: WindowBorderSize/ChildBorderSize/PopupBorderSize = 1, FrameBorderSize/TabBorderSize = 0 (Mantine no pinta border en frames) - **ImPlot** si esta linkado: FrameBg, PlotBg, PlotBorder, LegendBg/Border/Text, TitleText, InlayText, AxisText/Grid/Tick/Bg, Selection, Crosshairs + paddings ### Qué NO toca (capacidades ImGui intactas) - Ni `io.ConfigFlags` (docking, viewports, keyboard nav, multi-viewport): los gestiona `AppConfig` - Ni backends (GLFW/OpenGL init): los gestiona `app_base` - Ni `ImGui::CreateContext` / `DestroyContext` - Ni fuentes (si quieres Geist o una font custom, llama `io.Fonts->AddFontFromFileTTF(...)` antes de `apply_dark_theme`; el tema no pisa `io.Fonts`) - Ni `ImPlot` colormaps/markers (solo colores de chrome) --- ## 6. Equivalencias `@fn_library` ↔ C++ | Web (`@fn_library`) | C++ (`cpp/functions/...`) | Notas | |---|---|---| | `FnMantineProvider` + `createTheme` | `fn::run_app` con `ThemeMode::FnDark` | Punto de entrada unico | | `Button` | `core/button.h` — `fn::core::button(label, kind, size)` | `kind: Primary\|Secondary\|Subtle\|Danger`, `size: Sm\|Md\|Lg` | | `FnActionIcon` | `core/icon_button.h` | 28x28 + tooltip | | `Badge` | `core/badge.h` | `Default\|Success\|Warning\|Error\|Info\|Outline` | | `Input` / `TextInput` | `core/text_input.h` | label muted arriba | | `Select` | `core/select.h` | | | `Dialog` | `core/modal_dialog.h` | patron begin/end | | `Toast` / `notifications.show` | `core/toast.h` | stack bottom-right con fade-out | | `PageHeader` | `core/page_header.h` | begin/end para meter acciones | | `EmptyState` | `core/empty_state.h` | icono grande + titulo + desc | | `Sheet` / `Drawer` | `core/sidebar.h` | panel lateral colapsable | | `Tabs` | `core/tab_container.h` | | | `AppShell` | `core/docking_layout.h` | preset de docking space | | `Card` / `Paper` | `core/dashboard_panel.h` | surface + border + radius md | | `SimpleGrid` | `core/dashboard_grid.h` | N columnas uniformes | | `FnTree` / nav anidada | `core/tree_view.h` | | | `KPICard` | `viz/kpi_card.h` | valor + delta + sparkline | | `Sparkline` | `viz/sparkline.h` | mini chart inline | | `LineChart` | `viz/line_plot.h` | | | `BarChart` | `viz/bar_chart.h` | | | `AreaChart` | `viz/line_plot.h` + flags ImPlot | | | `PieChart` | `viz/pie_chart.h` | pie/donut | | `DataTable` | `viz/table_view.h` | sorting + scroll | | `GraphContainer` | `viz/graph_viewport.h` + `viz/graph_renderer.h` | sigma → ImGui GPU | | `Tabler icons` | Glyphs UTF-8 en `icon_button` | evitar bundles de icon atlas en apps pequeñas | --- ## 7. Plantilla `main.cpp` para una app nueva ```cpp #include "app_base.h" #include "imgui.h" #include "core/tokens.h" #include "core/fullscreen_window.h" #include "core/page_header.h" #include "core/dashboard_grid.h" #include "viz/kpi_card.h" static void render() { fullscreen_window_begin("##my_app"); page_header_begin("My App", "subtitle opcional"); page_header_end(); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); dashboard_grid_begin(4); kpi_card("Total", "1,284", /* delta */ 12.0f, /* positive */ true); kpi_card("OK", "97.3%", 0.4f, true); kpi_card("P95", "842ms", -8.0f, true); kpi_card("Err", "34", 5.0f, false); dashboard_grid_end(); fullscreen_window_end(); } int main() { fn::AppConfig cfg; cfg.title = "my_app"; cfg.width = 1400; cfg.height = 900; return fn::run_app(cfg, render); } ``` CMakeLists minimo de la app: ```cmake add_imgui_app(my_app main.cpp ${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp ${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp ${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp ${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp ) ``` `fn_framework` ya incluye `tokens.cpp`, no hace falta añadirlo. --- ## 8. Anti-patrones (rechazar en review) ```cpp // ❌ Hardcode de paleta ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.4f, 0.9f, 1.0f)); // ✅ ImGui::PushStyleColor(ImGuiCol_Button, fn_tokens::colors::primary); // ❌ StyleColorsDark en la app — sobreescribe la identidad ImGui::StyleColorsDark(); // ❌ apply_dark_theme en cada frame (no es caro pero es ruido) if (!theme_applied) { fn_tokens::apply_dark_theme(); ... } // ✅ run_app lo hace una vez al inicio // ❌ Spacing magico ImGui::Dummy(ImVec2(0, 13)); // ✅ ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); // ❌ Mezclar Qt, Nuklear u otra lib de UI en apps/ // ✅ ImGui + ImPlot + los wrappers de cpp/functions/ // ❌ Meter un sub-tema para "destacar" una pantalla cfg.theme = fn::ThemeMode::ImGuiLight; // romperia consistencia // ✅ usar tokens semanticos (success/warning/...) ``` --- ## 9. Checklist para una app C++ nueva - [ ] `main.cpp` llama a `fn::run_app(...)` con `ThemeMode::FnDark` (default) — **no** `ImGui::StyleColorsDark` - [ ] Cero `ImVec4(0.x, 0.y, ..., 1.0f)` — solo `fn_tokens::colors::*` - [ ] Cero floats magicos en padding/spacing/rounding — solo `fn_tokens::spacing::*` / `radius::*` - [ ] Componentes comunes desde `cpp/functions/core/`, `viz/`, `gfx/` - [ ] Si creas un widget reutilizable, extraelo a `cpp/functions//{name}.{h,cpp,md}` y `./fn index` - [ ] `app.md` con frontmatter (`name`, `description`, `tags`, `lang: cpp`) - [ ] CMakeLists usa `add_imgui_app(...)` — no `add_executable` directo - [ ] Multi-viewport (`cfg.viewports = true`) si la app espera ser undock-able; el tema se adapta solo - [ ] Tracy opt-in solo si interesa profiling (`-DTRACY_ENABLE=ON`) --- ## 10. Cambios respecto al estado previo (2026-04) - Tokens alineados a valores **exactos** de Mantine v9 dark + indigo (antes aproximaban a ojo) - `radius::md` ahora es 8 (antes 5) para casar con `defaultRadius: 'md'` - `app_base::AppConfig` añade campo `theme` (default `FnDark`) y aplica el tema en `run_app` - `fn_framework` incluye `tokens.cpp` — cualquier app que enlace el framework obtiene la identidad por default - `fn_tokens::apply_dark_theme()` ahora estiliza tambien ImPlot si esta enlazado (colores de plot bg, axis, legend, crosshairs) y añade entradas que faltaban (docking preview, nav highlight, modal dim, scrollbars) - `registry_dashboard` y `shaders_lab` ahora comparten look 100% sin cambios en sus propios mains --- ## 11. Iconos — Tabler (auto-cargado) Espejo del frontend web (`@tabler/icons-react`). Set unico para todas las apps C++ del registry. **Sin glyph atlas custom por app, sin emojis Unicode arbitrarios.** ### Regla Cualquier glyph en boton, menu, toolbar, tree o status line **DEBE** salir de los macros `TI_*` de `cpp/functions/core/icons_tabler.h`. No se permite: - `"\xe2\x9a\x99"` o cualquier UTF-8 hex inline → usar `TI_SETTINGS`. - Emojis Unicode (✓, ⚠, ❌, ⚙) → muchos no estan en el atlas y salen como `?` o cuadritos. - `"?"` / `"!"` ASCII como sustituto → usar `TI_HELP`, `TI_ALERT_CIRCLE`. ### Como funciona `fn::run_app` (en `framework/app_base.cpp`) llama a `fn_ui::load_default_fonts()` justo despues de crear el contexto ImGui. Carga **Karla-Regular** (texto vectorial, 13 px, vendoreado por ImGui en `vendor/imgui/misc/fonts/`) + mergea `tabler-icons.ttf` (5093 glyphs, U+E000..U+FCFF) al mismo tamaño en el mismo `ImFont`. Texto e iconos comparten line-height. A partir de ahi: ```cpp #include "core/icons_tabler.h" button(TI_PLUS " New", V::Primary); button(TI_DEVICE_FLOPPY " Save", V::Secondary); icon_button("##del", TI_TRASH, "Delete"); icon_button("##cfg", TI_SETTINGS, "Settings"); ``` Texto y glyph se mezclan en la misma string porque comparten atlas. ### Catalogo `cpp/functions/core/icons_tabler.h` (generado por `cpp/vendor/tabler-icons/gen_header.py` desde el CSS oficial v3.41.1). Buscar un icono: ```bash grep -i "TI_TRASH\|TI_DELETE" cpp/functions/core/icons_tabler.h # o explorar en https://tabler.io/icons (mismo set) ``` Nombres: `TI_` derivado del nombre kebab-case del icono Tabler (`tabler-trash` → `TI_TRASH`, `tabler-device-floppy` → `TI_DEVICE_FLOPPY`). ### Path resolution en runtime `icon_font.cpp` busca `Karla-Regular.ttf` y `tabler-icons.ttf` con el mismo orden (primer match gana): 1. `./` (cwd / junto al exe — `add_imgui_app` copia ambas post-build) 2. `./assets/` 3. `$FN_ASSETS_DIR/` 4. `${FN_CPP_ROOT}/` (compile-time define): - Karla → `vendor/imgui/misc/fonts/Karla-Regular.ttf` - Tabler → `vendor/tabler-icons/tabler-icons.ttf` Fallbacks: si falta Karla, ImGui usa ProggyClean (bitmap, default historico). Si falta Tabler, los `TI_*` salen como cuadritos. Indicadores programaticos: `fn_ui::text_font_loaded()`, `fn_ui::tabler_font_loaded()`. ### Upgrade de version ```bash cd cpp/vendor/tabler-icons curl -sL -o tabler-icons.ttf "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@/dist/fonts/tabler-icons.ttf" curl -sL -o tabler-icons.css "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@/dist/tabler-icons.css" python3 gen_header.py # regenera ../../functions/core/icons_tabler.h ``` Bump tambien `TI_FONT_VERSION` queda actualizado automaticamente. ### Anti-patrones (rechazar en review) ```cpp // ❌ Hex UTF-8 inline — pre-Tabler, no renderiza en el atlas actual icon_button("##rl", "\xe2\x86\xbb", "Reload"); // ✅ icon_button("##rl", TI_REFRESH, "Reload"); // ❌ Emoji button("✓ OK", V::Primary); // ✅ button(TI_CHECK " OK", V::Primary); // ❌ Cargar otra fuente de iconos en el app (FontAwesome, Material...) io.Fonts->AddFontFromFileTTF("fa-solid.ttf", ...); // ✅ usar TI_* — ya esta cargado por fn::run_app // ❌ Bypass del path resolver hardcodeando una ruta absoluta io.Fonts->AddFontFromFileTTF("/home/lucas/.../tabler.ttf", ...); // ✅ confiar en load_default_fonts() / FN_ASSETS_DIR ``` ### Checklist (añadir a la del punto 9) - [ ] Cero `"\x..\x.."` en codigo de UI — solo `TI_*` de `core/icons_tabler.h` - [ ] Cero emojis Unicode en strings de boton / menu / toolbar - [ ] Si el icono que necesito no existe en Tabler, replantear el flujo (en general hay equivalente) - [ ] No cargar fuentes adicionales sin justificacion (el atlas merge es delicado) --- ## 12. Settings — ventana flotante + persistencia Toda app C++ del registry hereda de `fn::run_app` un menu **Settings...** en la `MainMenuBar` (junto a `View` / `Layouts`) que abre una ventana flotante con: - **Display** → toggle `Show FPS overlay` (overlay top-right con fps + ms) - **Typography** → combo de fuente (Karla / Roboto / DroidSans / Cousine / ProggyClean) + combo de tamaño (12/13/14/15/16/18/20 px) + slider libre 10..32 px - **Secciones extra registradas por la app** (si las hay) ### Persistencia — `app_settings.ini` Junto al ejecutable. Auto-save al cambiar (sin botones). Cada app tiene su propio `.ini` porque cada exe corre desde su carpeta: ```ini # fn_registry app_settings.ini — autogenerado, editable show_fps = 1 font_id = 0 # 0=Karla 1=Roboto 2=DroidSans 3=Cousine 4=ProggyClean font_size_px = 15.0 ``` ### Extender desde una app ```cpp #include "core/app_settings.h" int main() { fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{ ImGui::Checkbox("Auto-compile on save", &g_auto_compile); ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000); }); return fn::run_app({...}, render); } ``` La seccion aparece debajo de las core, dentro de un `CollapsingHeader` abierto por defecto. La app es responsable de persistir su estado (su SQLite, env, archivo aparte) — el `.ini` core solo guarda `show_fps`/`font_id`/`font_size_px`. ### Cambio de fuente en runtime `run_app` consume `settings_consume_font_dirty()` al inicio de cada frame: si hay cambio, ejecuta `io.Fonts->Clear()` + `load_fonts_from_settings()`. ImGui 1.92+ refresca la GPU texture automaticamente via `ImGui_ImplOpenGL3_UpdateTexture` al siguiente `NewFrame`. Coste: un frame de hiccup, sin reinicio. ### Defaults - `font_id = Karla` — humanist sans-serif vectorial, vendoreado por ImGui (cero descargas), nitida a 13..18 px. - `font_size_px = 15` — un punto por encima de ImGui default (13) para legibilidad sin sacrificar densidad. Si una app necesita mas detalle, el usuario lo bumpea a 18/20. - `show_fps = false` — overlay opcional, no contamina capturas por defecto. ### Anti-patron ```cpp // ❌ Fps overlay hardcoded en el render — el toggle de Settings no lo apaga fps_overlay(); // ✅ El overlay lo gestiona run_app via settings().show_fps. La app no llama nada. // ❌ AddFontFromFileTTF directo en main — pisa load_fonts_from_settings io.Fonts->AddFontFromFileTTF("MyFont.ttf", 14); // ✅ Si necesitas una fuente fija no-configurable, hazlo en una nueva FontId // y un nuevo archivo .ttf vendoreado, no en codigo de app. ```