diff --git a/cpp/DESIGN_SYSTEM.md b/cpp/DESIGN_SYSTEM.md new file mode 100644 index 00000000..edc8197a --- /dev/null +++ b/cpp/DESIGN_SYSTEM.md @@ -0,0 +1,451 @@ +# 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. +```